A JNDI DataSource is a database connection factory that an application server creates and registers under a name, such as java:comp/env/jdbc/cities. The application asks for the name and never sees the JDBC URL or the password.
We mock a JNDI DataSource when production code looks up a JNDI name and we want to run that code in a JUnit test. For example, a city directory app runs on Tomcat with a MySQL pool, while its tests need the same Hibernate setup on H2. In a JUnit test there is no server, so nobody registers that name and the lookup fails. To mock the DataSource, we register a small in-memory JNDI provider and bind an H2 DataSource under the production name, so Hibernate looks it up as usual.
The following example binds an H2 DataSource in an in-memory provider and bootstraps Hibernate with the production JNDI name.
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, InMemoryContextFactory.class.getName());
new InitialContext().bind("java:comp/env/jdbc/cities", h2); // h2 = an H2 JdbcDataSource
EntityManagerFactory emf = new HibernatePersistenceConfiguration("cities")
.managedClasses(City.class)
.nonJtaDataSource("java:comp/env/jdbc/cities") // the production JNDI name
.schemaToolingAction(Action.CREATE_DROP)
.createEntityManagerFactory(); // create table City (...)
emf.runInTransaction(em -> em.persist(new City("Lisbon", "Portugal", 545000)));
// insert into City (country,name,population,id) values (?,?,?,?)
Notice that the Hibernate settings contain only the JNDI name, so the production bootstrap code runs unchanged in the test.
Next, we see why the lookup fails outside a server and write a JNDI provider in one class. After that, we repeat the test with Simple-JNDI and skip JNDI by passing the DataSource object to Hibernate.
1. What Is a JNDI DataSource?
JNDI (Java Naming and Directory Interface) is a JDK API in the javax.naming package. It stores objects under names, much like a shared Map<String, Object>. A DataSource is an object that returns JDBC connections, in most setups from a connection pool.
An application server such as Tomcat creates the pool from its own configuration (for Tomcat, a Resource element in context.xml) and binds it in its JNDI registry. Hibernate gets only the name and looks the object up at startup.

Hibernate reads the name, or the object itself, from one of three settings.
| Setting | Value | Used for |
|---|---|---|
| hibernate.connection.datasource | JNDI name or DataSource object | Hibernate’s own key (JdbcSettings.DATASOURCE) |
| jakarta.persistence.nonJtaDataSource | JNDI name or DataSource object | Transactions that the application starts and commits; nonJtaDataSource(String) sets it |
| jakarta.persistence.jtaDataSource | JNDI name | Transactions managed by a Jakarta EE server (JTA) |
With a DataSource setting, the Hibernate configuration contains no JDBC URL, driver or password. The server owns these details.
1.1. Why the Lookup Fails Outside a Server
The constructor new InitialContext() does not know any registry by itself. It searches for the name of an InitialContextFactory class, the provider that creates the registry. Inside an application server, the server registers its own provider. In a plain JVM, no code registers a provider, so the lookup throws a NoInitialContextException.
Object dataSource = new InitialContext().lookup("java:comp/env/jdbc/cities"); // NoInitialContextException
javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or in an application resource file: java.naming.factory.initial
The message lists the three places where JNDI searches for the provider class, and the first match wins.

| Where | How we set it in a test | Who sees it |
|---|---|---|
| Environment Hashtable | hibernate.jndi.class in the Hibernate settings | Only Hibernate’s lookups |
| System property | System.setProperty(Context.INITIAL_CONTEXT_FACTORY, …) in @BeforeAll | All code in the JVM |
| jndi.properties | A file in src/test/resources | All code in the test classpath |
So mocking a JNDI DataSource takes two steps. We register a provider in one of these places, then bind the DataSource under the name the code looks up.
2. Mocking a JNDI DataSource in a JUnit Test
The following example is a small city directory. Production code bootstraps Hibernate with the JNDI name java:comp/env/jdbc/cities, and JUnit 6 tests run it against an in-memory H2 database with Hibernate 7.4 and Java 25. The complete project on GitHub contains every class of the example and 17 JUnit tests (mvn -q compile exec:java, mvn test).
2.1. The City Entity and the Dependencies
A city has a name, a country and a population. The @Entity mapping has nothing JNDI-specific.
@Id
@GeneratedValue
private Long id;
private String name;
private String country;
private long population;
The JNDI API is part of the JDK (module java.naming), so the provider we write next needs no extra dependency. Hibernate, H2 and JUnit are enough.
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>7.4.11.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.5.252</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope> <!-- version from junit-bom 6.1.3 -->
</dependency>
2.2. A JNDI Provider in One Class
An InitialContextFactory has one method, getInitialContext(), which returns the registry. Ours returns a context backed by one static map, so every new InitialContext() in the JVM sees the same bindings.
public class InMemoryContextFactory implements InitialContextFactory {
private static final Map<String, Object> BINDINGS = new ConcurrentHashMap<>();
@Override
public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException {
return new InMemoryContext();
}
public static void clear() {
BINDINGS.clear();
}
static class InMemoryContext extends InitialContext {
InMemoryContext() throws NamingException {
super(true); // lazy: no provider search
}
@Override
public Object lookup(String name) throws NamingException {
Object value = BINDINGS.get(name);
if (value == null) {
throw new NameNotFoundException(name + " is not bound");
}
return value;
}
@Override
public Object lookup(Name name) throws NamingException {
return lookup(name.toString()); // Hibernate calls this one
}
@Override
public void bind(String name, Object value) {
BINDINGS.put(name, value);
}
@Override
public NameParser getNameParser(String name) {
return CompositeName::new; // Hibernate parses the name first
}
@Override
public void close() {
// keep the bindings: Hibernate closes its context after every lookup
}
}
}
Extending InitialContext with the protected InitialContext(true) constructor saves us from implementing the 29 methods of the Context interface. Hibernate calls getNameParser(), lookup(Name) and close(), so these three must work, and close() must not delete the bindings. The project version also overrides rebind() and unbind().
2.3. Binding the H2 DataSource
H2 ships a plain DataSource class, JdbcDataSource. DB_CLOSE_DELAY=-1 keeps the in-memory database alive while the JVM runs, even when no connection is open.
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:mem:cities;DB_CLOSE_DELAY=-1");
dataSource.setUser("sa");
dataSource.setPassword("");
The test registers the provider with a system property and binds the DataSource once, before Hibernate starts.
@BeforeAll
static void bindDataSource() throws NamingException {
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, InMemoryContextFactory.class.getName());
h2 = Database.h2("cities");
new InitialContext().bind("java:comp/env/jdbc/cities", h2);
emf = Database.fromJndi("java:comp/env/jdbc/cities", false);
}
@AfterAll
static void unbindDataSource() {
emf.close();
InMemoryContextFactory.clear();
System.clearProperty(Context.INITIAL_CONTEXT_FACTORY);
}
boolean same = new InitialContext().lookup("java:comp/env/jdbc/cities") == h2; // true
Bind the DataSource before creating the EntityManagerFactory. Hibernate looks the name up at bootstrap and fails at that point when the name is missing.
2.4. Bootstrapping Hibernate With the JNDI Name
The production bootstrap code does not change. It passes the name to nonJtaDataSource() of HibernatePersistenceConfiguration and gives no URL, user or password.
return new HibernatePersistenceConfiguration("cities")
.managedClasses(City.class)
.nonJtaDataSource(jndiName) // "java:comp/env/jdbc/cities"
.schemaToolingAction(Action.CREATE_DROP)
.showSql(showSql, false, false)
.createEntityManagerFactory();
create sequence City_SEQ start with 1 increment by 50
create table City (id bigint not null, population bigint not null, country varchar(255), name varchar(255), primary key (id))
A plain JDBC query on the bound h2 object finds the rows that Hibernate wrote, which proves that Hibernate used our DataSource and not a connection of its own.
2.5. Running CRUD in JUnit 6
Every test starts from the same three cities. The method truncate() of the Jakarta Persistence 3.2 SchemaManager empties the table before each test.
@BeforeEach
void loadCities() {
emf.getSchemaManager().truncate();
lisbonId = emf.callInTransaction(em -> {
City lisbon = new City("Lisbon", "Portugal", 545000);
em.persist(lisbon);
em.persist(new City("Porto", "Portugal", 232000));
em.persist(new City("Madrid", "Spain", 3300000));
return lisbon.getId();
});
}
The tests then use em.persist(), em.find(), a JPQL query and em.remove(), each in its own transaction. All of them go through the DataSource from JNDI.
| Operation | Code | SQL | Result |
|---|---|---|---|
| Create | em.persist(new City(“Lisbon”, …)) | insert into City (country,name,population,id) values (?,?,?,?) | 3 rows |
| Read | em.find(City.class, lisbonId) | select … from City c1_0 where c1_0.id=? | Lisbon (Portugal, 545000) |
| Update | find(…).setPopulation(548000) | update City set country=?,name=?,population=? where id=? | 548000 |
| Query | select c.name from City c where c.country = :country order by c.name | select c1_0.name from City c1_0 where c1_0.country=? order by c1_0.name | [Lisbon, Porto] |
| Delete | em.remove(em.find(City.class, lisbonId)) | delete from City where id=? | 2 rows left |
@Test
void queryByCountry() {
List<String> portugal = emf.callInTransaction(em -> em
.createQuery("select c.name from City c where c.country = :country order by c.name", String.class)
.setParameter("country", "Portugal")
.getResultList());
assertEquals(List.of("Lisbon", "Porto"), portugal);
}
@Test
void removeDeletesTheCity() {
emf.runInTransaction(em -> em.remove(em.find(City.class, lisbonId)));
assertNull(emf.callInTransaction(em -> em.find(City.class, lisbonId)));
}
2.6. The Same Test With Simple-JNDI
Simple-JNDI is an in-memory JNDI library; its latest release is 0.25.0 from February 2025. Instead of Java code, it creates the objects from properties files. We add it in test scope.
<dependency>
<groupId>com.github.h-thurow</groupId>
<artifactId>simple-jndi</artifactId>
<version>0.25.0</version>
<scope>test</scope>
</dependency>
The file jndi.properties in src/test/resources registers the provider and points to a folder with the object definitions.
java.naming.factory.initial=org.osjava.sj.SimpleContextFactory
org.osjava.sj.root=src/test/resources/jndi
org.osjava.sj.jndi.shared=true
org.osjava.sj.delimiter=/
org.osjava.sj.space=java:comp/env
org.osjava.sj.jndi.ignoreClose=true
Each settings line changes one default of Simple-JNDI.
- The key org.osjava.sj.root is the folder with the definition files. A file name becomes part of the JNDI name.
- The setting org.osjava.sj.jndi.shared=true makes every new InitialContext() see the same objects.
- The setting org.osjava.sj.delimiter=/ allows names with slashes, as in Tomcat.
- The setting org.osjava.sj.space=java:comp/env puts every object under java:comp/env.
- The setting org.osjava.sj.jndi.ignoreClose=true keeps the objects when Hibernate closes its context.
The file jdbc.properties in that folder declares the H2 DataSource. The file name jdbc plus the key cities gives the JNDI name java:comp/env/jdbc/cities.
cities/type=javax.sql.DataSource
cities/driver=org.h2.Driver
cities/url=jdbc:h2:mem:simple;DB_CLOSE_DELAY=-1
cities/user=sa
cities/password=
DataSource ds = (DataSource) new InitialContext().lookup("java:comp/env/jdbc/cities");
String text = ds.toString(); // org.h2.Driver::::jdbc:h2:mem:simple;DB_CLOSE_DELAY=-1::::sa
EntityManagerFactory emf = Database.fromJndi("java:comp/env/jdbc/cities", false); // same CRUD test passes
Both test classes are in one project. InMemoryJndiTest sets the system property, which wins over this jndi.properties file, and clears it in @AfterAll.
Without org.osjava.sj.jndi.ignoreClose=true, Simple-JNDI’s close() destroys the shared contexts. Hibernate closes its context after the lookup, so an object we bound with bind() is gone afterwards (NameNotFoundException), and a DataSource from a file is created again as a new object on the next lookup.
3. Passing the DataSource Without JNDI
Often the only code that looks up the JNDI name is the Hibernate configuration. Then the test can skip JNDI completely and hand the DataSource object to Hibernate. Both jakarta.persistence.nonJtaDataSource and hibernate.connection.datasource accept the object as well as a name.
return new HibernatePersistenceConfiguration("cities")
.managedClasses(City.class)
.property(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE, dataSource) // the object, not a name
.schemaToolingAction(Action.CREATE_DROP)
.createEntityManagerFactory();
try (EntityManagerFactory emf = Database.fromDataSource(Database.h2("direct"), false)) {
emf.runInTransaction(em -> em.persist(new City("Lisbon", "Portugal", 545000)));
long rows = count(emf); // 1
}
This test passes even with no provider registered and no jndi.properties on the classpath. Production keeps fromJndi(), and a test or a framework calls fromDataSource(). The choice depends on who performs the lookup.

| Option | Extra dependency | Setup | Best when |
|---|---|---|---|
| Own InitialContextFactory | None | One class, bind() in @BeforeAll | Code under test calls lookup(); we want full control and no library |
| Simple-JNDI 0.25.0 | simple-jndi (test scope) | jndi.properties plus one file per context | Many objects, or names already listed in files; set ignoreClose=true |
| Hibernate hibernate.jndi.class | None | One setting per EntityManagerFactory | Only Hibernate should see the test provider |
| DataSource object, no JNDI | None | property(JAKARTA_NON_JTA_DATASOURCE, ds) | Only the Hibernate setup uses the JNDI name |
| Spring SimpleNamingContextBuilder | spring-test 5.x only | Removed in Spring 6.0 | Not available on Spring 6 or 7 |
4. JNDI DataSource Testing FAQs
4.1. Why Does Hibernate Fail With “Unable to lookup JNDI name”?
A provider exists, but the name is not bound in it. Hibernate stops at bootstrap and wraps the JNDI error.
org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to lookup JNDI name [java:comp/env/jdbc/towns]
Caused by: org.hibernate.engine.jndi.JndiException: Unable to lookup JNDI name [java:comp/env/jdbc/towns]
Caused by: javax.naming.NameNotFoundException: java:comp/env/jdbc/towns is not bound
In tests, the name is missing for one of these reasons.
- The bound name and the configured name differ, for example jdbc/cities vs java:comp/env/jdbc/cities.
- The test binds the DataSource after it creates the EntityManagerFactory.
- The provider deletes bindings in close(), as Simple-JNDI does without ignoreClose=true.
4.2. What Replaced Spring’s SimpleNamingContextBuilder?
Older tests used SimpleNamingContextBuilder from spring-test to activate an in-memory JNDI registry. Spring deprecated it in 5.2 “in favor of complete solutions from third parties such as Simple-JNDI”, and the whole org.springframework.mock.jndi package is missing from spring-test 6.0.0 and 7.0.9. On Spring 6 or 7, we pass the DataSource object to Hibernate, and when our own code calls lookup(), we use Simple-JNDI or a small own factory.
4.3. How Do I Use a Test JNDI Provider for Hibernate Only?
We set hibernate.jndi.class. Hibernate passes every setting that starts with hibernate.jndi. to its own InitialContext, and this environment wins over the system property and jndi.properties.
EntityManagerFactory emf = new HibernatePersistenceConfiguration("cities-jndi-class")
.managedClasses(City.class)
.nonJtaDataSource("java:comp/env/jdbc/cities")
.property("hibernate.jndi.class", InMemoryContextFactory.class.getName())
.createEntityManagerFactory(); // finds the 3 cities from InMemoryContextFactory
In the project test, the system property and jndi.properties both name Simple-JNDI at that moment, and Hibernate still uses InMemoryContextFactory. Code that calls new InitialContext() itself does not see this setting.
5. Conclusion
A mock JNDI DataSource keeps the production configuration intact. The test registers an in-memory provider and binds an H2 DataSource under the real name before Hibernate starts, so the same CRUD code runs against H2. A small InitialContextFactory needs no dependency, and Simple-JNDI 0.25.0 declares the objects in files. When only the Hibernate setup reads the JNDI name, passing the DataSource object to jakarta.persistence.nonJtaDataSource is simpler than any JNDI mock.
6. References
- Hibernate ORM 7.4 User Guide: Using DataSources
- Hibernate ORM 7.4 User Guide: JNDI
- PersistenceConfiguration JavaDoc (Jakarta Persistence 3.2)
- InitialContext JavaDoc (Java 25)
- InitialContextFactory JavaDoc (Java 25)
- Tomcat 11 JNDI Datasource How-To
- Simple-JNDI on Maven Central
Happy Learning !!