A Hibernate in-memory database test runs our entities and queries against a database such as H2 that keeps its tables and rows in the memory of the JVM, so it starts in milliseconds and is gone when the tests finish. Hibernate creates the tables from the entities, loads a few rows from import.sql, and runs the real SQL of our queries without a database server.
We use an in-memory database to unit test mappings, repositories and JPQL queries on every build, on a developer laptop or a CI agent that has no MySQL or PostgreSQL installed.
The following example configures Hibernate for an in-memory H2 database and adds the JUnit lifecycle methods that build one EntityManagerFactory per test class and give every test a fresh copy of the data.
EntityManagerFactory emf = new HibernatePersistenceConfiguration("invoices-test")
.managedClasses(Invoice.class)
.jdbcUrl("jdbc:h2:mem:invoices;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1")
.jdbcCredentials("sa", "")
.schemaToolingAction(Action.CREATE_DROP)
.createEntityManagerFactory(); // create table Invoice (...)
// + every insert from import.sql
@BeforeAll static void createFactory() { emf = Database.inMemory(true); } // create table + import.sql
@BeforeEach void resetData() { emf.getSchemaManager().truncate(); } // truncate table + import.sql
@AfterEach void closeEntityManager() { em.getTransaction().rollback(); } // discard the test's changes
@AfterAll static void closeFactory() { emf.close(); } // drop table if exists Invoice
Notice that the expensive factory is built once per class, whereas the cheap truncate() call runs before every test and reloads import.sql.
Next, we see what an in-memory test catches and build the invoice example step by step. The FAQs at the end cover the H2 surprises, such as vanishing tables and missing rows.
1. What Does an In-memory Database Test Catch?
A Hibernate test can run against a mock EntityManager that returns canned objects, against an in-memory database inside the JVM, or against the real database server. Only the last two execute SQL, so only they find a wrong column name or a broken JPQL query (JPQL is the object query language of Jakarta Persistence).
For example, a developer renames the field issuedOn to issueDate in the entity but forgets the JPQL query in findUnpaid(). A test with a mock EntityManager still passes, because no query runs. A test against H2 fails on the next build, because Hibernate cannot resolve issuedOn in the query.
| Question | Mock EntityManager | In-memory H2 | Real database (Testcontainers) |
|---|---|---|---|
| Runs real SQL? | No | Yes, H2’s SQL | Yes, production SQL |
| Finds mapping and JPQL errors? | No | Yes | Yes |
| Finds database-specific SQL errors? | No | No, H2 is not MySQL | Yes |
| Needs Docker or a server? | No | No | Docker |
| Startup cost | None | One factory: 120 to 175 ms in our runs | Container start before the first test |
H2 gives fast tests for mappings, repositories and JPQL queries, but it cannot prove that MySQL or PostgreSQL accepts our native SQL. We compare H2 and a real database in more detail in section 3.
2. Hibernate In-memory Database Test Example
The following example is a small invoicing app for a freelancer, built with Hibernate 7.4, Java 25, H2 2.5 and JUnit 6. An Invoice has a client, an amount, an issue date and a paid flag, and an InvoiceRepository reads and changes invoices.
2.1. Maven Dependencies
Only the tests need H2, so it goes in the test scope next to JUnit. The JUnit BOM (bill of materials, a pom that only lists versions) keeps all JUnit modules on the same version.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.2. The Entity and the Repository
The Invoice class is a plain @Entity with a generated ID. The amount is a BigDecimal and the issue date a LocalDate.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String client;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal amount;
@Column(name = "issued_on", nullable = false)
private LocalDate issuedOn;
private boolean paid;
The repository gets an EntityManager in its constructor. Constructor injection makes the repository testable, because the test decides which database and which transaction the repository uses.
public Invoice save(Invoice invoice) {
em.persist(invoice);
return invoice;
}
public List<Invoice> findUnpaid() {
return em.createQuery("from Invoice where paid = false order by issuedOn", Invoice.class)
.getResultList();
}
public BigDecimal outstandingFor(String client) {
return em.createQuery(
"select coalesce(sum(amount), 0) from Invoice where client = :client and paid = false",
BigDecimal.class)
.setParameter("client", client)
.getSingleResult();
}
public int markAllPaid(String client) {
return em.createQuery("update Invoice set paid = true where client = :client")
.setParameter("client", client)
.executeUpdate();
}
2.3. Configuring Hibernate for H2
The JDBC URL decides how H2 behaves. The prefix mem: makes it an in-memory database, and the settings after each semicolon change its behavior.

The class HibernatePersistenceConfiguration builds the factory in Java, without a persistence.xml file. The value Action.CREATE_DROP drops and creates the tables when the factory starts and drops them again when it closes, the same as hibernate.hbm2ddl.auto=create-drop.
public static final String H2_URL =
"jdbc:h2:mem:invoices;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1";
public static HibernatePersistenceConfiguration h2(String url) {
return new HibernatePersistenceConfiguration("invoices-test")
.managedClasses(Invoice.class)
.jdbcUrl(url)
.jdbcCredentials("sa", "")
.schemaToolingAction(Action.CREATE_DROP);
}
public static EntityManagerFactory inMemory(boolean showSql) {
return h2(H2_URL)
.showSql(showSql, false, false)
.createEntityManagerFactory();
}
drop table if exists Invoice cascade
create table Invoice (amount numeric(10,2) not null, issued_on date not null, paid boolean not null,
id bigint generated by default as identity, client varchar(255) not null, primary key (id))
Older tutorials build a SessionFactory from a hibernate-test.cfg.xml file with StandardServiceRegistryBuilder. HibernatePersistenceConfiguration (new in Hibernate 7) does the same job in a few lines of Java and returns a standard Jakarta Persistence factory.
2.4. Loading Test Data With import.sql
Hibernate runs the file import.sql from the root of the classpath right after it creates the tables. In a Maven project, the file goes to src/test/resources/import.sql.
insert into invoice (client, amount, issued_on, paid) values ('Lokesh', 400.00, '2026-09-01', true);
insert into invoice (client, amount, issued_on, paid) values ('Lokesh', 250.00, '2026-09-15', false);
insert into invoice (client, amount, issued_on, paid) values ('Alex', 120.00, '2026-09-20', false);
Hibernate has more than one way to pick a load script, and the options combine in different ways. We loaded the same one-row file extra-invoices.sql (an invoice for Maria) with each setting, and the last column shows which rows were in the table.
| Setting | Standard? | Rows after startup |
|---|---|---|
| None, import.sql on the classpath | Hibernate only | Lokesh, Lokesh, Alex |
| jakarta.persistence.sql-load-script-source = extra-invoices.sql | Jakarta Persistence | Maria, Lokesh, Lokesh, Alex (both files run) |
| hibernate.hbm2ddl.import_files = extra-invoices.sql | Hibernate only | Maria (replaces import.sql) |
EntityManagerFactory emf = Database.h2(Database.H2_URL)
.property("jakarta.persistence.sql-load-script-source", "extra-invoices.sql")
.createEntityManagerFactory();
Hibernate reads load scripts one line per statement. An insert split over two lines fails with a syntax error that Hibernate only logs as a warning, so the test starts with missing rows. The fix is in FAQ 4.3.
2.5. One EntityManagerFactory per Test Class, Fresh Data per Test
Building an EntityManagerFactory is the slow part, because Hibernate reads the mappings and creates the tables. So we build it once in @BeforeAll and close it in @AfterAll. Each test still needs the same starting data, so the @BeforeEach method empties the tables and loads import.sql again.

The call emf.getSchemaManager().truncate() is new in Jakarta Persistence 3.2. It truncates the mapped tables and re-imports the initial data from the configured load scripts, and the SQL log shows Hibernate doing both.
private static EntityManagerFactory emf;
private EntityManager em;
private InvoiceRepository repository;
@BeforeAll
static void createFactory() {
emf = Database.inMemory(true); // create tables + run import.sql
}
@BeforeEach
void resetData() {
emf.getSchemaManager().truncate(); // empty tables + run import.sql again
em = emf.createEntityManager();
em.getTransaction().begin();
repository = new InvoiceRepository(em);
}
@AfterEach
void closeEntityManager() {
if (em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
em.close();
}
@AfterAll
static void closeFactory() {
emf.close(); // drop tables
}
set referential_integrity false
truncate table Invoice
set referential_integrity true
insert into invoice (client, amount, issued_on, paid) values ('Lokesh', 400.00, '2026-09-01', true)
insert into invoice (client, amount, issued_on, paid) values ('Lokesh', 250.00, '2026-09-15', false)
insert into invoice (client, amount, issued_on, paid) values ('Alex', 120.00, '2026-09-20', false)
The fields emf and em follow the JUnit rules. @BeforeAll methods are static by default, so emf is a static field, whereas JUnit creates a new test class instance, with new instance fields, for every test method, so the @BeforeEach method fills em each time.
2.6. Testing the Repository
Each test calls the repository and checks the result against the three invoices from import.sql. Because truncate() reloads the same rows before every test, the tests can run in any order.
List<Invoice> unpaid = repository.findUnpaid();
// [Lokesh 250.00 (open), Alex 120.00 (open)]
// select ... from Invoice i1_0 where i1_0.paid=false order by i1_0.issued_on
Invoice invoice = repository.save(new Invoice("Maria", "90.00", LocalDate.of(2026, 9, 25)));
Long id = invoice.getId(); // 4
// insert into Invoice (amount,client,issued_on,paid,id) values (?,?,?,?,default)
BigDecimal lokesh = repository.outstandingFor("Lokesh"); // 250.00
BigDecimal maria = repository.outstandingFor("Maria"); // 0 (no rows, coalesce)
int updated = repository.markAllPaid("Lokesh"); // 2
// update Invoice i1_0 set paid=true where i1_0.client=?
BigDecimal after = repository.outstandingFor("Lokesh"); // 0
Changes inside a test stay invisible to the next one, even when a test commits. In the following test, we delete every invoice and commit, and the next test still starts with three rows.
em.createQuery("delete from Invoice").executeUpdate();
em.getTransaction().commit();
Long count = em.createQuery("select count(*) from Invoice", Long.class).getSingleResult(); // 0
// next test: @BeforeEach truncates and reloads, count = 3
Besides the repository tests, the complete project on GitHub has 12 more tests that check the H2 behavior from the FAQs (18 tests in total) and a demo class that prints the SQL of each lifecycle step (mvn test, mvn -q test-compile exec:java).
2.7. Resetting Data Between Tests
The method truncate() is not the only way to give each test clean data. We can also build a new factory for every test, or roll back each test’s transaction. The strategies differ in speed and in what they clean up.
| Strategy | How | Cost per test (our runs) | Cleans up committed data? |
|---|---|---|---|
| New factory per test | Factory in @BeforeEach, create-drop | 120 to 175 ms | Yes, tables are dropped |
| truncate() per test | One factory, truncate() in @BeforeEach | 9 to 12 ms | Yes, rows are reloaded |
| Rollback per test | One factory, rollback() in @AfterEach | No extra SQL | No, see FAQ 4.6 |
The times come from two runs of the demo class (10 rounds of each strategy per run) and only show the order of magnitude. We use one factory per class with truncate() and keep the rollback as a second safety net, which is the setup in section 2.5.
3. H2 vs Testcontainers
Testcontainers starts the real database in a Docker container for the tests. It runs the same SQL as production, so it catches the errors H2 hides. The cost is Docker on every developer machine and CI agent, plus a container start before the first test. Say the invoice app later adds a monthly report that uses a MySQL window function in a native query. H2 cannot prove that query works, whereas a Testcontainers test runs it on MySQL 8.4.
| Point | In-memory H2 | Testcontainers |
|---|---|---|
| Setup | One test dependency | Docker plus a module such as testcontainers-mysql |
| Speed | Starts inside the JVM | Container start first, then normal speed |
| SQL dialect | H2, with a compatibility mode | The production database and version |
| Native queries, functions, procedures | Fail or behave differently | Same as production |
| Good for | Mappings, repositories, JPQL, fast feedback | Native SQL, database features, final integration tests |
With the Testcontainers JDBC driver on the test classpath, only the URL changes. The Testcontainers configuration needs Docker, so it is not part of the example project.
EntityManagerFactory emf = new HibernatePersistenceConfiguration("invoices-test")
.managedClasses(Invoice.class)
.jdbcUrl("jdbc:tc:mysql:8.4:///invoices") // Testcontainers starts mysql:8.4 in Docker
.schemaToolingAction(Action.CREATE_DROP)
.createEntityManagerFactory();
Many projects use both. They run H2 for the fast unit tests of repositories, and Testcontainers for the classes that use native SQL or stored procedures.
4. In-memory Database Testing FAQs
4.1. Why Does My H2 Table Disappear Between Connections?
By default, H2 deletes an in-memory database when its last connection closes. A table created over one connection is gone when the next connection opens, and H2 reports that the database is empty.
-- connection 1
create table invoice (id int); -- then close the connection
-- connection 2
select * from invoice;
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "INVOICE" not found (this database is empty); SQL statement:
select * from invoice [42104-252]
The setting DB_CLOSE_DELAY=-1 keeps the database until the JVM stops, and the same two connections then find the table. Hibernate’s built-in connection pool keeps at least one connection open while the factory lives (Hibernate logs Minimum pool size: 1 at INFO level when the factory starts), so DB_CLOSE_DELAY=-1 matters most for test code that opens its own JDBC connections.
4.2. Why Does a Native Query Fail on H2 but Work on MySQL?
H2 accepts much of MySQL’s syntax in MODE=MySQL, but it does not have every MySQL function. For example, the following native query uses the MySQL function DATE_FORMAT().
List<?> months = em.createNativeQuery("select date_format(issued_on, '%Y-%m') from invoice").getResultList(); // SQLGrammarException
org.hibernate.exception.SQLGrammarException: Could not prepare statement [Function "date_format" not found; SQL statement:
select date_format(issued_on, '%Y-%m') from invoice [90022-252]]
When the query can be written in JPQL instead, Hibernate generates the SQL for the configured dialect, so the query needs no database-specific function.
List<Object[]> totals = em.createQuery("""
select extract(month from issuedOn), sum(amount)
from Invoice group by extract(month from issuedOn)""", Object[].class)
.getResultList(); // [[9, 770.00]]
A native query that must stay native belongs in a Testcontainers test.
4.3. Why Are the Rows From import.sql Missing?
The default script reader (SingleLineSqlScriptExtractor) treats every line as one statement. A statement that spans two lines is cut at the line break, H2 rejects the first half, and Hibernate only logs a warning.
insert into invoice (client, amount, issued_on, paid)
values ('Maria', 90.00, '2026-09-25', false);
WARN org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl - GenerationTarget encountered exception accepting command : Error executing DDL "insert into invoice (client, amount, issued_on, paid)" via JDBC [Syntax error in SQL statement "insert into invoice (client, amount, issued_on, paid)[*]"; ...]
We either keep each statement on one line, or switch to the multi-line reader.
.property("hibernate.hbm2ddl.import_files_sql_extractor",
"org.hibernate.tool.schema.internal.script.MultiLineSqlScriptExtractor")
4.4. Why Do IDs Not Start at 1 After Each Test?
H2’s TRUNCATE TABLE keeps the identity counter in its regular mode. The rows come back, but with new IDs. In MODE=MySQL, H2 restarts the counter, as MySQL does.
| URL | IDs after truncate() |
|---|---|
| jdbc:h2:mem:invoices;MODE=MySQL;… | 1, 2, 3 |
| jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1 | 4, 5, 6 |
Tests that look up rows by a fixed ID depend on this setting, so we either use MODE=MySQL or find the rows by a business value such as the client name.
4.5. Why Does persist() Fail With a Primary Key Violation After Loading Data?
The load script inserted a row with a fixed ID, but H2’s identity counter did not move. The next em.persist() gets ID 1 again and fails.
insert into invoice (id, client, amount, issued_on, paid) values (1, 'Maria', 90.00, '2026-09-25', false);
org.hibernate.exception.ConstraintViolationException: could not execute statement [Unique index or primary key violation: "PUBLIC.CONSTRAINT_9 PRIMARY KEY ON PUBLIC.INVOICE(ID) ( /* key:1 */ 90.00, DATE '2026-09-25', FALSE, CAST(1 AS BIGINT), 'Maria')"; SQL statement:
insert into Invoice (amount,client,issued_on,paid,id) values (?,?,?,?,default) [23505-252]]
With MODE=MySQL, H2 moves the counter past an inserted ID, and the next invoice gets ID 2. The cleanest fix in any mode is to leave the id column out of the load script, as our import.sql does.
4.6. Is Rolling Back the Transaction After Each Test Enough?
No, the rollback is enough only when every change goes through the test’s own transaction. When the code under test commits in its own transaction, for example with emf.runInTransaction(), the rollback in @AfterEach cannot undo that commit.
em.getTransaction().begin();
emf.runInTransaction(other -> // code under test commits on its own
other.persist(new Invoice("Maria", "90.00", LocalDate.of(2026, 9, 25))));
em.getTransaction().rollback();
// Maria is still in the table: 4 invoices
For that reason our setup also truncates in @BeforeEach, so the next test starts with the three rows from import.sql, whatever the previous test committed.
5. Conclusion
An in-memory H2 database lets us unit test Hibernate mappings, repositories and JPQL queries with real SQL and without a database server. We build one EntityManagerFactory per test class, load the data from import.sql, call truncate() before each test, and pick the H2 URL settings on purpose, such as DB_CLOSE_DELAY=-1 to keep the database and MODE=MySQL to get closer to MySQL. For native SQL and database features, a Testcontainers test against the real database is the right tool.
6. References
- H2 Database: In-Memory Databases
- H2 Database: MySQL Compatibility Mode
- SchemaManager JavaDoc (Jakarta Persistence 3.2)
- Hibernate ORM 7.4 User Guide: Schema generation
- Hibernate ORM 7.4 User Guide: hibernate.hbm2ddl.import_files
- JUnit 6 User Guide: Test Instance Lifecycle
- Testcontainers: JDBC support
Happy Learning !!
I believe you may have an erroneous “1” in the password
hibernate.connection.password”>1
When I removed it, it worked.
Thanks none-the-less!
Thank you DtothekK, I faced the same problem :)
here is an error in hibernate.cfg.xml
org.hsqldb.jdbcDriver
The name of class is supposed to start with capital later
Code provided is correct. Please refer to : http://hsqldb.org/doc/guide/
Hello Lokesh,
I am wondering why select query is not working in my case.
I mean when i write code to select all data from the data base or say row,It is just executing the select query but list size is 0.why so? your example is show only insert operation.please give me brief about this to how to do it.
You must do insert and select.. both in single session. Please confirm if you are doing it in single session.
Thanks Lokesh, Its very helpful. I just came to know that, we can use hibernate to connect IM database like any other RDBMS by this tutorial.
I have a couple of questions here.
#1. Do we need to install HSql DB separetly? How ? (Some info links would be helpful :))
#2. If i want to use this HSQL data base(or any other IM db) for my Junits, Do i need to dump all my database objects and testdata into this database? Or any other way i can use existing DB as IM DB?
Why I am asking this is, we are working in a project where most of the business logic reside at DB Procedures. I am thinking in a way how can we make our junits without depends on our database(Oracle).
Thanks in Advance. Please Help.
1) For in memory usage, you do not need to install HSQLDB. Just include its jar file as I have added through maven.
2) I guess you will need to dump all your data before execution. It’s in-memory so once it’s not in use, it will be destroyed including DB schema.
If you not want to use external database, then recommended practice is to use mocking.
Thanks again lokesh.
Do u have any idea, what are all other IM databases which is supporting by hibernate4?
Help me with some tutorial links to explore on Hsql db.
There is one more database H2 database, I know of. Try googling for hsqldb related content, because that’s what I will do as well :-)
Thanks for very helpful tutorial. I’m new in Java and Hibernate as well. I managed to compile the code and understood the concept as well. But the problem is that I don’t know how to execute the code to debug some stuff. When I start app as java application in eclipse. I have a long list to of option. But I don’t see my actual test class to run. So How to run it and debug this app? Please see the link I asked same question in the stackoverflow @ https://stackoverflow.com/questions/26563177/using-in-memory-database-with-hibernate-tutorial-how-to-execute
As this was example only so I was executing it directly from main() method in TestHibernate.java.
hi guys,
i am using Hibernate 4.3.5
i followed the tutorial,
i can see on the logs that the tables are created
but i am having this error on the first insert.
“java.sql.SQLSyntaxErrorException: user lacks privilege or object not found: ADVISORID
did anyone faced the same error ?
thanks
Tried this solution?
thank you for the reply,
in fact , the problem was a definition for a certain column on my entity
// [Causing problem] @Column(name = "ADV_LANGUAGE", nullable = false, columnDefinition = "CHAR(3) NOT NULL DEFAULT 'FR'") private String lang; // [Fixing problem] @Column(name = "ADVT_LANGUE", nullable = false, length = 3) private String lang;thank you for this great tuto, helped a lot.