Mock JNDI DataSource in JUnit Tests with Hibernate and H2

Test code that looks up a JNDI DataSource without an application server: register a small in-memory JNDI provider or Simple-JNDI, bind an H2 DataSource under the production name, bootstrap Hibernate 7 with it and run CRUD in JUnit 6. Or skip JNDI and pass the DataSource object.

Jakarta_EE

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.

Two columns: in production Hibernate calls lookup with java:comp/env/jdbc/cities, the Tomcat JNDI registry filled from context.xml returns a DataSource backed by a Tomcat connection pool and a MySQL server; in a JUnit test the same Hibernate settings look up the same name in InMemoryContextFactory, filled by the test in @BeforeAll, which returns an H2 JdbcDataSource for an H2 database in JVM memory
The test keeps the JNDI name and the Hibernate settings. Only the registry and the DataSource behind the name change.

Hibernate reads the name, or the object itself, from one of three settings.

SettingValueUsed for
hibernate.connection.datasourceJNDI name or DataSource objectHibernate’s own key (JdbcSettings.DATASOURCE)
jakarta.persistence.nonJtaDataSourceJNDI name or DataSource objectTransactions that the application starts and commits; nonJtaDataSource(String) sets it
jakarta.persistence.jtaDataSourceJNDI nameTransactions 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.

Flow: new InitialContext searches java.naming.factory.initial in 1 the environment Hashtable (hibernate.jndi.class for Hibernate), 2 the system property, 3 jndi.properties on the classpath; none found leads to NoInitialContextException; a found factory creates the context, and lookup of java:comp/env/jdbc/cities returns the DataSource when bound or NameNotFoundException, wrapped in JndiException, when not bound
No provider gives NoInitialContextException. A provider without the name gives NameNotFoundException.
WhereHow we set it in a testWho sees it
Environment Hashtablehibernate.jndi.class in the Hibernate settingsOnly Hibernate’s lookups
System propertySystem.setProperty(Context.INITIAL_CONTEXT_FACTORY, …) in @BeforeAllAll code in the JVM
jndi.propertiesA file in src/test/resourcesAll 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.

OperationCodeSQLResult
Createem.persist(new City(“Lisbon”, …))insert into City (country,name,population,id) values (?,?,?,?)3 rows
Readem.find(City.class, lisbonId)select … from City c1_0 where c1_0.id=?Lisbon (Portugal, 545000)
Updatefind(…).setPopulation(548000)update City set country=?,name=?,population=? where id=?548000
Queryselect c.name from City c where c.country = :country order by c.nameselect c1_0.name from City c1_0 where c1_0.country=? order by c1_0.name[Lisbon, Porto]
Deleteem.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.

Decision tree: if only Hibernate looks up the name, pass the DataSource object with JAKARTA_NON_JTA_DATASOURCE, no JNDI provider and no extra library; if the code under test calls new InitialContext().lookup itself, a JNDI provider is needed: an own InMemoryContextFactory with bind() in @BeforeAll for objects declared in Java code, or Simple-JNDI 0.25.0 with jndi.properties, jdbc.properties and ignoreClose=true for objects declared in files; Spring SimpleNamingContextBuilder is no longer an option, deprecated in 5.2 and removed in 6.0
If only Hibernate does the lookup, pass the object. If our own code calls lookup(), register a provider.
OptionExtra dependencySetupBest when
Own InitialContextFactoryNoneOne class, bind() in @BeforeAllCode under test calls lookup(); we want full control and no library
Simple-JNDI 0.25.0simple-jndi (test scope)jndi.properties plus one file per contextMany objects, or names already listed in files; set ignoreClose=true
Hibernate hibernate.jndi.classNoneOne setting per EntityManagerFactoryOnly Hibernate should see the test provider
DataSource object, no JNDINoneproperty(JAKARTA_NON_JTA_DATASOURCE, ds)Only the Hibernate setup uses the JNDI name
Spring SimpleNamingContextBuilderspring-test 5.x onlyRemoved in Spring 6.0Not 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

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.