Your DAO works fine in production because the application server — WildFly, Payara, GlassFish — provides the JNDI lookup for its DataSource. Run the same code from a plain JUnit test and you get javax.naming.NoInitialContextException: the JVM outside the container has no JNDI provider at all. You shouldn’t need to boot a real application server to run one test.
This article rebuilds that mock JNDI setup against Hibernate 7.4.5.Final and current library versions, and it corrects two dependency coordinates in the process that will otherwise cost you an hour of confused stack traces. It also reports an honest gap: one path this article originally set out to demonstrate could not be made to work, and that failure is more useful to know about than a fabricated success would have been.
Which part is for you
| Part | Read this if… |
|---|---|
| 1 — Beginner | You want a working bind-then-lookup with the current simple-jndi coordinates |
| 2 — Intermediate | Your lookup returns null, or “works alone, fails in the suite” and you need to know why |
| 3 — Advanced | You want the Spring Boot 4.1.1 specifics, the honest gap in this investigation, and whether mock JNDI is still worth reaching for in 2026 |
Verified against. Hibernate ORM 7.4.5.Final, Spring Boot 4.1.1 (GA 2026-07), Spring Framework 7.0.9, JDK 25, H2 2.4.240, and com.github.h-thurow:simple-jndi:0.25.0. Every failure message quoted below is verbatim from a real test run, not retyped.
Part 1 — A working bind-then-lookup
Get the dependency coordinates right first
The groupId that shows up in most existing tutorials, simple-jndi:simple-jndi, is an old, essentially abandoned fork. The maintained one is a different groupId entirely:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<!-- version managed by Spring Boot 4.1.1's BOM: 7.4.5.Final -->
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.h-thurow</groupId>
<artifactId>simple-jndi</artifactId>
<version>0.25.0</version>
<scope>test</scope>
</dependency>
Second correction, and this one produces a more confusing failure than a dependency-resolution error: the class name itself changed. The commonly-quoted org.osjava.sj.memory.MemoryContextFactory does not exist anywhere in the 0.25.0 jar — that whole package is gone. The real class lives one level up:
# unzip -l simple-jndi-0.25.0.jar | grep -i contextfactory
org/osjava/sj/MemoryContextFactory.class
org/osjava/sj/SimpleContextFactory.class
org/osjava/sj/SimpleJndiContextFactory.class
org/osjava/sj/ContextFactory.class
And the system property that carries it is java.naming.factory.initial, not java.naming.provider.url — a second, independent mistake that circulates in the same old examples. Full jar listing: docs/output/jndi-simplejndi-jar-listing.txt.
A working bind and lookup
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.osjava.sj.MemoryContextFactory");
System.setProperty("org.osjava.sj.jndi.shared", "true"); // Part 2 explains why this line matters
Context ctx = new InitialContext();
ctx.createSubcontext("java:");
ctx.createSubcontext("java:comp");
ctx.createSubcontext("java:comp/env");
ctx.createSubcontext("java:comp/env/jdbc");
JdbcDataSource ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:jnditestdb;DB_CLOSE_DELAY=-1");
ds.setUser("sa");
ds.setPassword("");
ctx.bind("java:comp/env/jdbc/TestDS", ds);
DataSource looked = (DataSource) new InitialContext().lookup("java:comp/env/jdbc/TestDS");
try (var conn = looked.getConnection()) {
// conn.isValid(2) -> true
}
And driving Hibernate itself through the same bound name, with no Spring involved — plain Configuration, the same shape of setup the container would otherwise provide for you:
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.datasource", "jdbc/HibernateTestDS")
.applySetting("hibernate.hbm2ddl.auto", "create-drop")
.build();
SessionFactory sessionFactory = new Configuration().buildSessionFactory(registry);
try (Session session = sessionFactory.openSession()) {
Integer result = session.createNativeQuery("SELECT 1", Integer.class).getSingleResult();
// result == 1
}
The verbatim log line proving this actually resolved the DataSource through JNDI, not a hardcoded URL:
HHH10001005: Database info:
DataSource JNDI name [jdbc/HibernateTestDS]
Database JDBC URL [jdbc:h2:mem:hibernate-jndi-test]
Pool: DataSourceConnectionProvider
hibernate.connection.datasource is a real, current setting — the constant DATASOURCE in org.hibernate.cfg.JdbcSettings, confirmed via javap against the 7.4.5.Final jar. When Hibernate sees it set, it ignores standard JDBC URL/user/password settings entirely and performs the JNDI lookup itself. Full test: HibernateJndiDataSourceTest.
Part 2 — How it works, and how to debug it when it doesn’t
The property every abbreviated example skips
org.osjava.sj.jndi.shared=true is the line in Part 1 that looks optional and isn’t. javap -c on MemoryContextFactory.class shows the factory branches on that exact property name to decide whether to consult a static, JVM-shared context cache. Without it, every single new InitialContext() call gets back a brand-new, empty MemoryContext — so a bind() through one InitialContext instance is invisible to a lookup() through a different instance, even inside the same test method, if the code happens to construct InitialContext more than once. This was not a hypothetical risk while building this article’s companion tests — it’s the first failure this investigation actually hit.
The failure modes, verbatim
No java.naming.factory.initial set at all:
Need to specify class name in environment or system property, or in an application resource file: java.naming.factory.initial
Looking up a name nothing ever bound — the message is just the name itself:
javax.naming.NameNotFoundException: java:comp/env/jdbc/DoesNotExist
And binding a name a previous test already bound and never cleaned up:
javax.naming.NameAlreadyBoundException: Name jdbc/SharedAcrossTests already bound. Use rebind() to override
Full captures: docs/output/jndi-tests-run.txt.
“Name already bound”: reproduced by accident, then on purpose
This surfaced for real while building the companion repository, not as a staged example. Once all three JNDI test classes in this article ran together in one Surefire invocation instead of one at a time, a different test’s @BeforeEach started throwing NameAlreadyBoundException on ctx.createSubcontext("jdbc") — another test class in the same run had already created that subcontext and never removed it. Simple-JNDI’s shared, static, JVM-wide namespace isn’t a toy problem confined to a contrived demo; it’s the library’s normal behavior, and it bit a real suite the first time that suite ran as a whole.
The message’s own suggestion — rebind() instead of bind() — makes the immediate error go away, but it’s a band-aid: it papers over the leak rather than closing it. The actual fix has two parts, both applied in this repo’s tests: make subcontext creation idempotent (catch NameAlreadyBoundException, treat “already there” as success), and unbind whatever a test binds, unconditionally, in @AfterEach. Do the second part alone and you still leak subcontexts across classes; do the first part alone and leaf-name pollution still crosses test class boundaries.
Transaction management: keep it simple unless you’re specifically testing JTA
If your application code runs under JTA, mocking JNDI alone isn’t enough — you’d also need a mock transaction manager (Bitronix, Atomikos) registered in JNDI too, which is a lot of moving parts for a unit test. For most mock-JNDI tests, set hibernate.transaction.coordinator_class=jdbc and drive transactions directly with session.beginTransaction()/commit(). Reach for JTA mocking only when the thing under test is XA transaction behavior itself.
Part 3 — What this doesn’t cover, and whether it’s still the right tool
Spring’s own mock JNDI helper: gone since 2022, not a recent change
org.springframework.mock.jndi.SimpleNamingContextBuilder is the “just use Spring” answer you’ll find elsewhere, so it’s worth settling with evidence rather than repeating it. It’s present in spring-test-5.3.31.jar (8 class files, including the builder itself) and gone completely from spring-test-6.0.0.jar and spring-test-7.0.9.jar — zero matches for naming or jndi in either jar. It went in the same Spring Framework 6.0 release that moved the whole framework from javax.* to jakarta.*, and there is no direct built-in replacement in spring-test itself. That’s the actual reason a third-party library like simple-jndi is still the practical answer. Confirmed via unzip -l against all three jars: docs/output/jndi-simplenamingcontextbuilder-removal.txt.
javax.naming was never renamed. Unlikejavax.persistenceandjavax.servlet, JNDI is a Java SE API shipped in thejava.namingmodule, not a Jakarta EE one — it did not move tojakarta.namingin the EE 9 migration and has no reason to. simple-jndi’sMemoryContextFactoryimplementsjavax.naming.spi.InitialContextFactorytoday and will keep doing so unless the JDK itself changes the package. Anyone going looking for ajakarta.namingartifact is chasing a rename that never happened.
Boot 4.1.1: spring.datasource.jndi-name still exists, but it moved, and one path stayed unverified
javap against the real 4.1.1 jars confirms both pieces are alive: org.springframework.boot.jdbc.autoconfigure.JndiDataSourceAutoConfiguration with its dataSource(DataSourceProperties, ApplicationContext) factory method, and DataSourceProperties.jndiName backing the property, getter and setter intact. The relocation matters if you go grepping for it: this lives in the separate spring-boot-jdbc module now, not spring-boot-autoconfigure — that jar has zero matches for jndi at all in 4.1.1, part of the same autoconfigure-module split that moved JPA’s autoconfiguration into spring-boot-jpa.
What this investigation could not get working: a full @SpringBootTest actually resolving spring.datasource.jndi-name end-to-end through Boot’s own autoconfiguration. Standalone simple-jndi bind/lookup worked repeatedly, including through Spring’s own JndiTemplate called directly — that’s the mechanism Part 1’s Hibernate-native example relies on. But inside a real ApplicationContext refresh, the autoconfigured dataSource bean’s own JNDI lookup consistently threw NameNotFoundException, even after confirming: the binding ran in a static initializer ahead of SpringExtension‘s callbacks; the classloader and System.identityHashCode of MemoryContextFactory were identical between the working standalone lookup and the failing in-context one; the relevant system properties were confirmed present and correct at the point of failure; and no stray jndi.properties or competing InitialContextFactoryBuilder registration turned up anywhere on a 107-jar test classpath. The root cause was not found. This is reported as an open question, not glossed over — the working demonstration in this article is deliberately plain Hibernate, not Spring Boot autoconfiguration, and that’s why.
Is mock JNDI still the right answer in 2026?
Being honest about what this investigation found: mock JNDI is a legacy technique, kept alive for the shrinking set of applications that still get deployed into a real Jakarta EE application server, 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 in 2026 — there’s no container JNDI tree to fake in the first place, so mocking one in tests solves a problem the production topology doesn’t have. If you’re starting new work on Boot, wire spring.datasource.url against an in-memory database (see the in-memory database article) or Testcontainers instead. Reach for simple-jndi specifically when the application under test really is deployed via JNDI in production and the test needs to mirror that exact lookup path — not as a generic “how do I mock a DataSource” answer.
Long tail
- Testcontainers as the alternative for dialect-accurate integration coverage — same trade-off as the in-memory-database article: milliseconds vs Docker-container accuracy.
- JTA/XA mock transaction managers (Bitronix, Atomikos) for the narrow case where you’re testing distributed-transaction behavior itself, not just a DataSource lookup.
Should you build this at all? Only if your production deployment target genuinely is a Jakarta EE container. If you’re on Spring Boot and just want fast, isolated persistence tests, skip JNDI mocking entirely and point spring.datasource.url at H2 — you get the same speed with one less moving part and no static, JVM-wide namespace to accidentally pollute between tests.
Further reading
- Repo chapter: 10 — Mocking JNDI DataSources
- Hibernate 7 In-Memory DB Testing — H2 setup without JNDI for simpler test scenarios
- Hibernate 7 SessionFactory Bootstrapping — programmatic configuration that works alongside JNDI mocking
- Hibernate 7 EntityManager Bootstrapping — JPA persistence unit configuration for test environments
- Official Hibernate 7 User Guide — DataSource configuration
No Comments yet!