Files
hibernate-demo/docs/10-mocking-jndi-datasources.md

172 lines
11 KiB
Markdown
Executable File

# 10 — Mocking JNDI DataSources, verified against Spring Framework 7.0.9 and simple-jndi 0.25.0
[← Previous: 09 — Testing with in-memory databases](09-testing-in-memory-databases.md) | [Next: 11 — Proxies and lazy initialization →](11-proxies-and-lazy-initialization.md)
Backs [ankurm.com: mocking JNDI datasources](https://ankurm.com/testing-hibernate-7-mocking-jndi-datasources-without-the-container/).
Companion code: [`src/test/java/com/ankurm/hibernatedemo/jndi/`](../src/test/java/com/ankurm/hibernatedemo/jndi/) (three test classes —
[`JndiDataSourceResolutionTest`](../src/test/java/com/ankurm/hibernatedemo/jndi/JndiDataSourceResolutionTest.java),
[`HibernateJndiDataSourceTest`](../src/test/java/com/ankurm/hibernatedemo/jndi/HibernateJndiDataSourceTest.java),
[`CrossTestPollutionTest`](../src/test/java/com/ankurm/hibernatedemo/jndi/CrossTestPollutionTest.java) —
seven tests, all green together in one run -- see [`docs/output/jndi-tests-run.txt`](output/jndi-tests-run.txt)
(filtered) and [`docs/output/jndi-full-run.txt`](output/jndi-full-run.txt) (unfiltered Surefire capture of the same
run). Environment: Hibernate ORM 7.4.5.Final, Spring Boot 4.1.1, Spring Framework 7.0.9, JDK 25,
H2 2.4.240, simple-jndi 0.25.0 (`com.github.h-thurow:simple-jndi`, test scope).
## `SimpleNamingContextBuilder`: gone, and it's not a recent change
The article this replaces doesn't use `SimpleNamingContextBuilder`, but it's the most commonly
recommended "just use Spring's mock JNDI" answer elsewhere, so it's worth settling with evidence.
`unzip -l` against the actual jars ([`docs/output/jndi-simplenamingcontextbuilder-removal.txt`](output/jndi-simplenamingcontextbuilder-removal.txt)):
- `spring-test-5.3.31.jar` (last of the 5.x line): `org/springframework/mock/jndi/` present,
8 class files including `SimpleNamingContextBuilder.class`.
- `spring-test-6.0.0.jar`: zero matches for `naming` or `jndi` anywhere in the jar.
- `spring-test-7.0.9.jar` (what this whole blog batch verifies against): same, zero matches.
So it went in Spring Framework 6.0.0 -- the same release that moved the whole framework from
`javax.*` to `jakarta.*` for Jakarta EE 9. There is **no direct built-in replacement** in
`spring-test` itself; the practical answer for the last several years has been a third-party
library, which is exactly why this chapter exists. One nuance worth stating precisely: JNDI
(`javax.naming.*`) is a **Java SE API** shipped in the `java.naming` module, not a Jakarta EE
API, so it did not get renamed to `jakarta.naming` the way `javax.persistence` and
`javax.servlet` did. simple-jndi's `MemoryContextFactory` still `implements
javax.naming.spi.InitialContextFactory` in 2026, and always will unless the JDK itself changes
it.
## Getting simple-jndi 0.25.0 actually working
First correction to the article's own dependency block: the artifact coordinates it used,
`simple-jndi:simple-jndi:0.11.4.1`, are an old, essentially abandoned groupId. The maintained
fork used throughout this blog batch is `com.github.h-thurow:simple-jndi:0.25.0`. Second: the
article's `jndi.properties` sets `java.naming.provider.url=org.osjava.sj.memory
.MemoryContextFactory` -- that package does not exist in the 0.25.0 jar at all
([`docs/output/jndi-simplejndi-jar-listing.txt`](output/jndi-simplejndi-jar-listing.txt)). The real class is
`org.osjava.sj.MemoryContextFactory`, and the property that should carry it is
`java.naming.factory.initial`, not `java.naming.provider.url`.
A working bind-then-lookup, from `JndiDataSourceResolutionTest`:
```java
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.osjava.sj.MemoryContextFactory");
System.setProperty("org.osjava.sj.jndi.shared", "true"); // see below -- this one is easy to miss
Context ctx = new InitialContext();
ctx.createSubcontext("java:"); ctx.createSubcontext("java:comp"); /* ...etc */
ctx.bind(JNDI_NAME, dataSource);
DataSource looked = (DataSource) new InitialContext().lookup(JNDI_NAME);
```
`org.osjava.sj.jndi.shared=true` is the detail every abbreviated example skips, and skipping it
produces a confusing failure: without it, `javap -c` on `MemoryContextFactory.class`
([`docs/output/proxy-settings-javap.txt`](output/proxy-settings-javap.txt)'s sibling investigation technique, applied here) shows
the factory branches on that exact property name and, if it's not `"true"`, hands back a **brand
new, empty** `MemoryContext` on every single `new InitialContext()` call instead of consulting
its static, JVM-shared cache. A `bind()` through one `InitialContext` instance is then invisible
to a `lookup()` through a different one -- even inside the same test method, if the code happens
to construct more than one `InitialContext`. This was not a hypothetical: it's exactly the first
failure this investigation hit.
Also demonstrated, and also driving Hibernate itself (not just a raw JDBC lookup): Hibernate's
`hibernate.connection.datasource` setting (constant `DATASOURCE` in `org.hibernate.cfg
.JdbcSettings`, confirmed via `javap`, [`docs/output/jndi-hibernate-datasource-setting-javap.txt`](output/jndi-hibernate-datasource-setting-javap.txt))
resolves a JNDI name into a real, working `SessionFactory` --
`HibernateJndiDataSourceTest` builds one and runs `SELECT 1` through it. Verbatim log line
proving the resolution actually went through JNDI, not a URL:
```
HHH10001005: Database info:
DataSource JNDI name [jdbc/HibernateTestDS]
Database JDBC URL [jdbc:h2:mem:hibernate-jndi-test]
...
Pool: DataSourceConnectionProvider
```
## The failure modes, verbatim
`NoInitialContextException` when `java.naming.factory.initial` is never set:
```
Need to specify class name in environment or system property, or in an application resource file: java.naming.factory.initial
```
`NameNotFoundException` on an unbound name -- message is just the name itself:
```
java:comp/env/jdbc/DoesNotExist
```
`NameAlreadyBoundException` across two tests sharing a JVM -- reproduced on purpose in
`CrossTestPollutionTest` (test A binds and never cleans up; test B tries to bind the same name):
```
Name jdbc/SharedAcrossTests already bound. Use rebind() to override
```
That message's own suggestion (`rebind()` instead of `bind()`) does make the immediate error go
away, but it is a band-aid, not the fix -- it papers over test A's leak rather than closing it.
`CrossTestPollutionTest`'s third test spells out the real fix: whatever a test binds, that same
test unbinds in `@AfterEach`, unconditionally, so nothing survives to the next test class in the
same JVM.
This surfaced for real, by accident, in this exact investigation: once all three JNDI test
classes ran together in one Surefire invocation instead of one at a time, `HibernateJndiDataSourceTest`'s
`@BeforeEach` started throwing `NameAlreadyBoundException` on `ctx.createSubcontext("jdbc")` --
a *different* test class in the same run had already created that subcontext and never removed
it. The fix applied ([`docs/output/jndi-tests-run.txt`](output/jndi-tests-run.txt) shows the resulting clean run) was to make
subcontext creation idempotent (catch `NameAlreadyBoundException`, treat "already there" as
success) in addition to unbinding leaf names in `@AfterEach`. Simple-JNDI's shared, static,
JVM-wide namespace is not a toy problem confined to a contrived demo -- it is the normal
behavior of the library, and it bit this test suite the first time the suite ran as a whole.
## Boot 4.1.1 specifics: `spring.datasource.jndi-name` is alive, relocated
`javap` against the actual 4.1.1 jars ([`docs/output/jndi-boot-autoconfig-javap.txt`](output/jndi-boot-autoconfig-javap.txt)) confirms
both pieces still exist:
- `org.springframework.boot.jdbc.autoconfigure.JndiDataSourceAutoConfiguration` -- a real class,
with a `dataSource(DataSourceProperties, ApplicationContext)` factory method.
- `DataSourceProperties.jndiName` -- the field backing `spring.datasource.jndi-name`, with its
getter/setter intact.
The relocation matters for anyone grepping for it in the wrong place: this is in the
**`spring-boot-jdbc`** module, not `spring-boot-autoconfigure` -- `spring-boot-autoconfigure-4.1.1.jar`
has zero matches for `jndi` at all. This is the same Boot 4 autoconfigure-module split noted
elsewhere in this batch (JPA landed in `spring-boot-jpa`; JDBC/datasource landed in
`spring-boot-jdbc`).
What did **not** get resolved in this sandbox: a full `@SpringBootTest` actually resolving
`spring.datasource.jndi-name` end-to-end through Boot's own `JndiDataSourceAutoConfiguration`.
Standalone simple-jndi bind/lookup worked perfectly (proven above, repeatedly, including through
Spring's own `JndiTemplate` called directly). But inside a real `ApplicationContext` refresh,
the `dataSource` bean's JNDI lookup consistently threw `NameNotFoundException` even though: the
binding was moved to a static initializer (to run before `SpringExtension`'s `BeforeAllCallback`,
which fires before a test class's own `@BeforeAll`); the class loading, `System.identityHashCode`,
and classloader of `MemoryContextFactory` were confirmed identical between the successful
standalone lookup and the failing in-context one via a diagnostic `BeanFactoryPostProcessor`; the
relevant system properties (`java.naming.factory.initial`, `org.osjava.sj.jndi.shared`) were
confirmed present and correct at the point of failure; and no `jndi.properties` resource or
JNDI `InitialContextFactoryBuilder` registration was found anywhere on the 107-jar test
classpath. The root cause was not found. `HibernateJndiDataSourceTest` (Hibernate's own
`hibernate.connection.datasource`, no Spring autoconfiguration involved) is the test that
carries the "Hibernate/Spring resolves it by name" claim for this chapter -- the
Boot-autoconfiguration-specific path is documented as class-and-property-exist-but-live-wiring-unverified,
not glossed over as working.
## Is mock JNDI still the right answer in 2026?
Being honest about what this investigation actually found: mock JNDI is a legacy technique kept
alive for the shrinking set of applications that still get deployed into a real Java EE/Jakarta
EE application server (WildFly, Payara) where a container-managed `DataSource` is genuinely the
only path to a connection. For anything running as a Spring Boot fat jar -- the overwhelming
majority of new work -- there is no container JNDI tree to fake in the first place, so mocking
one in tests is solving a problem the production topology doesn't have. The honest recommendation
for a Boot application in 2026 is what the rest of this repo already does (see
[chapter 09](09-testing-in-memory-databases.md)): an in-memory database
or Testcontainers wired through `spring.datasource.url`, not JNDI. Reach for simple-jndi
specifically when the application under test really is deployed via JNDI in production and the
test needs to mirror that lookup path -- not as a generic "how do I mock a DataSource" answer.
[← Previous: 09 — Testing with in-memory databases](09-testing-in-memory-databases.md) | [Next: 11 — Proxies and lazy initialization →](11-proxies-and-lazy-initialization.md)