H2 for Tests: Fast Feedback Loops
Your tests are too slow. You push code. You wait. Coffee gets cold. The build pings your screen after five to ten minutes and tells you a DAO broke on some tiny thing. This is common when you point your tests at a real database. Spinning up MySQL or Postgres for every test run is like towing a trailer to get groceries. It works. It is not fast.
Right now many teams run JUnit on Maven with Hudson or CruiseControl. They run Spring and Hibernate. The place where time goes is I O and boot time for the database. The fix is old school and practical. Use an in memory database for tests. H2 and HSQLDB are the usual picks on the Java side. Both are tiny. Both start in milliseconds. Both live inside your process which means near zero setup.
This write up is what I do on real projects. I will frame the problem, walk three cases from simple to full stack, then answer common pushback. If you want fast feedback loops, this is your path.
Problem framing
You want repeatable tests that run in seconds. You also want tests that catch the same mistakes your production database would catch. This pulls in two directions. Speed likes memory and light weight. Parity likes the exact engine you run in prod. Teams pick sides and get burned. There is a third way. Use H2 or HSQLDB for the tight loop on your laptop and CI build. Then run a nightly job against your real database to catch edge cases with SQL quirks.
H2 and HSQLDB are both strong here. H2 feels a bit faster on my box and has modes that mimic MySQL and Postgres pretty well. HSQLDB is mature and stable with great SQL coverage. You can swap one for the other by changing a JDBC URL and a dialect line in Hibernate.
Three case walkthrough
Case 1: Plain JDBC and JUnit
Start with the simplest thing. No Spring. No JPA. You just want to test a repository class that runs SQL. Add H2 as a test dependency and use the memory URL. The database starts when you first connect. It vanishes when the JVM exits.
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.0.2008</version>
<scope>test</scope>
</dependency>String url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1";
Connection c = DriverManager.getConnection(url, "sa", "");
// Create schema for each test
c.createStatement().execute("create table user_account(id int primary key, email varchar(255) not null)");
c.createStatement().execute("insert into user_account(id, email) values(1, 'ana@example.com')");
// run assertions...The trick above is DB CLOSE DELAY set to minus one so the memory database stays alive across connections during a single test run. With JUnit 4 you can use a @Before method to build the schema and a @After method to drop it. This gives you clean tests and no flakiness.
Case 2: Hibernate or JPA boot with auto schema
Most apps in Java land right now use Hibernate 3 or JPA. H2 works great with both. Point Hibernate to H2, set hbm2ddl to create drop for tests, and let it build the schema in memory. This is what fast feels like.
<property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.hbm2ddl.auto">create-drop</property>
<property name="hibernate.show_sql">false</property>
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<property name="hibernate.connection.url">jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</property>
<property name="hibernate.connection.username">sa</property>
<property name="hibernate.connection.password"></property>If you use annotations, it just works. If you use XML mappings, same story. Watch out for ID generation. H2 supports auto increment and sequences. If prod is MySQL, prefer identity style. If prod is Postgres, prefer sequences. Both are fine in H2, so pick the one that matches prod to cut surprises.
@Entity
public class UserAccount {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
}Run this with JUnit or TestNG. On my laptop this boots in under a second even with a dozen entities. That keeps the loop tight while you map fields and write queries.
Case 3: Spring tests with transactions and fixtures
Now the full stack case. Spring context, transaction manager, repositories, services. The Spring TestContext runner makes this sweet. Use a DataSource bean for H2 or HSQLDB and wrap tests in a transaction that rolls back by default. Your tests leave no dirt behind.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:/app-test.xml"})
@Transactional
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void registersNewUser() {
UserAccount u = userService.register("luis@example.com");
assertNotNull(u.getId());
}
}<!-- app-test.xml -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>hbm/UserAccount.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
<prop key="hibernate.hbm2ddl.auto">create-drop</prop>
</props>
</property>
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>For fixtures, keep a set of SQL scripts or use Liquibase to create tables and seed data. Liquibase lets you apply the same changesets to H2 or HSQLDB in tests and to prod later. One source of truth. One set of scripts. Fast plus repeatable.
H2 or HSQLDB
So which one. My short take:
H2 starts fast and has a MySQL mode and a Postgres mode that catch common quirks. The console is handy and the jar is tiny. Good for teams that want speed and a familiar feel if prod is MySQL.
HSQLDB is rock solid with a wide SQL feature set. If you care about strict types and want something closer to standards, it feels nice. If your prod queries are fancy, HSQLDB can be a better mirror.
You can run both in a build with Maven profiles. Use H2 for default tests. Use HSQLDB in a second job that runs less often. If a query breaks in one, you fix it once and move on.
<profiles>
<profile>
<id>test-h2</id>
<properties>
<db.url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</db.url>
<db.driver>org.h2.Driver</db.driver>
<hibernate.dialect>org.hibernate.dialect.H2Dialect</hibernate.dialect>
</properties>
</profile>
<profile>
<id>test-hsqldb</id>
<properties>
<db.url>jdbc:hsqldb:mem:test</db.url>
<db.driver>org.hsqldb.jdbcDriver</db.driver>
<hibernate.dialect>org.hibernate.dialect.HSQLDialect</hibernate.dialect>
</properties>
</profile>
</profiles>Objections and replies
Objection It is not the same as prod so tests lie.
Reply You are right that engines differ. That is why I add a nightly job against the real engine. The day to day loop must be fast or people stop running tests. We can have both. Fast unit and integration tests on H2 or HSQLDB during the day. A deeper run on MySQL or Postgres when we sleep.
Objection My team needs to test concurrency and locks.
Reply Memory drivers will not match disk engines for locks. Split this into two suites. Most code does not need heavy lock tests. For the small part that does, point at a real database in an overnight job and use stress tools. Keep the main suite fast.
Objection Our schema uses stored procedures and vendor functions.
Reply If logic lives in the database, mirror those parts with an embedded engine is hard. Still, you can keep service and mapping tests in H2 or HSQLDB and mark procedure tests to run against prod in CI. Mixed strategy beats all or nothing.
Objection Keeping test data in sync is a pain.
Reply Use one source of truth. Store schema and seed data in version control. Apply them with Liquibase or plain SQL at test start. Never rely on a hand built database. Builds become repeatable across laptops and CI. That is the win you want.
Action oriented close
The target is simple. Make the loop tight. You push code and get a green or red answer before your browser finishes loading a news site about the shiny new Chrome release. Here is a short plan you can run this week.
Steps:
- Add H2 as a test scoped dependency. Or add HSQLDB if you prefer.
- Point your ORM or JDBC layer to a memory URL. Create schema on test start.
- Wrap tests in a transaction and roll back by default. Spring TestContext makes this easy.
- Move schema and seed SQL into version control. Use Liquibase if you want change logs.
- Add a Maven profile for prod engine and run it nightly on Hudson or CruiseControl.
- Watch build time. Keep it under one minute for the default test run.
If you want a tiny example, drop this into a new Maven module and run mvn test. You will see a green test in a blink.
public class SmokeTest {
@Test
public void simpleInsert() throws Exception {
Connection c = DriverManager.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", "");
Statement s = c.createStatement();
s.execute("create table t(id int primary key, name varchar(100))");
s.execute("insert into t(id, name) values(1, 'ok')");
ResultSet rs = s.executeQuery("select name from t where id=1");
rs.next();
assertEquals("ok", rs.getString(1));
}
}That is the feel you want across your suite. Fast. Predictable. Zero setup. The long jobs can run while you sleep. The short jobs keep you honest while you code.
One last tip. Name the rule. Tell the team tests must finish before you can sip your coffee. If the cup gets cold, fix the suite. That simple rule keeps projects healthy. It also keeps your mornings happy.
If you have been waiting for a sign to switch, this is it. H2 or HSQLDB will take your test time from minutes to seconds. The feedback loop matters. Ship faster with less drama.