172 lines
11 KiB
Markdown
Executable File
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)
|