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

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 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). 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 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): 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 →