An in-memory jester in King Java's court
Published on 2022-05-09
Summary
The pursuit of test coverage must not detract from the Developer Experience. This guide shows how to speed up JUnit test suites by using the in-memory H2 database instead of brawnier options like PostgreSQL that are better suited for production.
It is rare to come across someone who creates software for fun or profit that genuinely looks forward to writing tests. Perhaps some prudently heeded the wisdom of their elders and took to Test-Driven Development like ducks to water, but I admit the benefits of testing early & often had to be beaten into my workflow by the vicissitudes of botched deployments and after-hours rollbacks. That said, test coverage — which should remain a measure and never a goal — must not come at the expense of ergonomics. Long setup and teardown times are the first domino in a chain reaction that leads to impatience being distilled into cut corners, pipelines where tests are run selectively, and frustration in the long term.
One of the most effective ways of reducing the reluctance towards writing tests and having suites run quickly is to prevent database operations from becoming a bottleneck. It is here that in-memory solutions truly shine. The present guide will walk you through integrating the embedded H2 database engine with your JUnit suite and harnessing it to speed up tests.
Contents
Prerequisites
You will need a Java application tested with JUnit, such as a web backend that uses the Spring framework. The real-life use case I will base this guide on is my experience adding H2 to the test suite for Exodus , an open-source migration runner I created to manage database changes in Spring applications.
Setup
Add H2 as a dependency in your POM as shown below. Get the latest version number from H2 on MavenRepository and set the <version>
property accordingly.
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.212</version>
<scope>test</scope>
</dependency>
Create an in-memory database
You will create a fresh in-memory database for each test suite, allowing it to be garbage collected at the end of each run. First, let's declare the objects you will inject into tests in order to mock the database:
public class MyTestSuite {
DataSource dataSource;
Connection conn;
Statement statement;
Next, create a constructor for your test suite (or modify the existing one) as such:
public MyTestSuite() {
dataSource = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
try {
conn = dataSource.getConnection();
statement = conn.createStatement();
} catch (SQLException e) {
e.printStackTrace();
}
}
The embedded database is instantiated and assigned to dataSource
and the conn
& statement
objects are derived from it. The latter two will be passed to methods that run SQL statements on the database from your tests. This guide will later look at running arbitrary SQL as a string or from a file handled as a Spring Resource.
Manage the test life cycle
Next, decorate the test suite class with @TestInstance(Lifecycle.PER_CLASS)
. This will ensure that JUnit runs all test methods within a suite against the same instance, avoiding the overhead of creating a datasource and related objects anew for each test method.
However, this means that state is carried over between tests in the in-memory database, which would muddy the waters and stop tests from being independent from each other. In order to reset the database between test method runs you can create a method annotated with JUnit's decorator:
@BeforeEach
private void beforeEach() {
try {
statement.execute("DROP ALL OBJECTS;");
} catch (SQLException e) {
e.printStackTrace();
}
}
H2 supports a special SQL statement DROP ALL OBJECTS
that will delete all existing tables, views, and schemas. An advantage of using this command as opposed to TRUNCATE TABLE
is that the former will also take care of resetting sequences (such as those of SERIAL
fields) with a negligible difference in performance.
This guide will revisit the beforeEach()
method later in order to capture and test log output.
Populate the test database
There are two ways of interacting with the H2 database: by building a string containing SQL statements or, if using Spring, by applying a script stored as a Spring Resource. The first approach is to call statement.execute("...")
as you have already seen in beforeEach()
above. Arbitrary SQL can be run in this way in order to construct schemas and populate them.
If using the Spring framework you may wish to move away from manually constructing SQL statements in your test code to storing more complex database scripts as separate files in order to avoid cluttering the JUnit suites. SQL scripts can be applied to the database in a test context as such:
import org.springframework.jdbc.datasource.init.ScriptUtils;
[...]
Resource sqlScript = new ClassPathResource("db/create_schema.sql");
ScriptUtils.executeSqlScript(conn, sqlScript);
The path passed to ClassPathResource()
is relative to src/test/resources/
. By applying .sql files in this manner you can quickly set up a test database's schema and fill it with data while keeping the resources to do so organised in a logical structure.
Caveat emptor
Before using H2 familiarise yourself with the data types it supports. While it is standards-compliant, watch out for discrepancies between types that may exist in the database engine used by your application but not so in H2. For instance, PostgreSQL's TEXT is not a type in H2, so an alternative schema that uses VARCHAR (equivalent under the hood) is necessary.
Query the database in tests
It is also possible to query the database and use the results in your tests. For instance, let's look at retrieving a list of names:
ArrayList<String> names = new ArrayList<>();
ResultSet result = statement.executeQuery("SELECT u.first_name FROM users u;");
while (result.next()) {
names.add(result.getString("first_name"));
}
By creating a collection such as an empty ArrayList, executing a query, and looping through the results, you can add any data to the collection. The result.next()
instruction moves the cursor through the ResultSet until all rows have been exhausted (this is similar to looping through a generator in Python, if that is something you are familiar with). You can now use JUnit assertions against the names
ArrayList to test the result of this query.
Capture & test log output
The following is not directly related to H2 or mocking databases, but rather a nugget you may find useful in order to test log output with JUnit. If using the Spring Boot starter the Logback
& SLF4J
dependencies will already be satisfied. Otherwise, add them to your POM.
First, create a logger and a container to hold logging events as members of your test class:
Logger logger = (Logger) LoggerFactory.getLogger(MigrationRunner.class);
List<ILoggingEvent> logList;
Now, return to the beforeEach()
method:
@BeforeEach
private void beforeEach() {
[...]
logger.detachAndStopAllAppenders();
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
logger.addAppender(listAppender);
listAppender.start();
logList = listAppender.list;
}
Before each test method you should instantiate a ListAppender
, remove any existing appenders from the logger and attach the new one, directing this appender's output to logList
.
Application code can be invoked as usual in test cases and the instrumentation you have just created will intercept it on the fly. You may test any log output as such:
assertEquals(2, logList.size());
assertEquals("This is the first log message.", logList.get(0).getMessage());
You have now:
- Sped up your JUnit tests by using an in-memory database.
- Integrated H2 into your test life cycle with
hooks.
- Captured logs and tested them.
I hope this guide has been of service and has helped you reduce overhead in your test suites. If you want to see how all the above comes together in a real-life project — or are looking for a light (~7 kb) & simple migrations runner for your next Spring application — you should check out Exodus on GitHub .
Main image made using Inkscape.
Dropcap background: Violet and Columbine (1883) by William Morris, licensed under CC0.