11 KiB
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 | Next: 11 — Proxies and lazy initialization →
Backs ankurm.com: mocking JNDI datasources.
Companion code: src/test/java/com/ankurm/hibernatedemo/jndi/ (three test classes —
JndiDataSourceResolutionTest,
HibernateJndiDataSourceTest,
CrossTestPollutionTest —
seven tests, all green together in one run -- see docs/output/jndi-tests-run.txt
(filtered) and docs/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):
spring-test-5.3.31.jar(last of the 5.x line):org/springframework/mock/jndi/present, 8 class files includingSimpleNamingContextBuilder.class.spring-test-6.0.0.jar: zero matches fornamingorjndianywhere 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). 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:
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'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)
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 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) confirms
both pieces still exist:
org.springframework.boot.jdbc.autoconfigure.JndiDataSourceAutoConfiguration-- a real class, with adataSource(DataSourceProperties, ApplicationContext)factory method.DataSourceProperties.jndiName-- the field backingspring.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): 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 | Next: 11 — Proxies and lazy initialization →