Database Tests With DbUnit (Part 1)

Inspired by a recent blog post and presentation by Jens Schauder, this blog posts starts a series of posts about using DbUnit for database and integration tests. In addition, I will talk about Database Tests with DbUnit at ObjektForum Karlsruhe in April. This first post introduces DbUnit and demonstrates how it can be used to write database tests.

For tests that use a database it is crucial to be able to set up the database to a known state before each test run. Otherwise such tests tend to be very fragile and will require a lot of manual care to keep them green over time as they will often fail due to different database contents.

Suppose we want to test a class called PersonRepository that loads instances of Person from the database. For example, the method findPersonByFirstName should load the (first) person with a specified first name. How can we test this method? Our first test might look like this:

@Test
public void findsAndReadsExistingPersonByFirstName() throws Exception {
	PersonRepository repository = new PersonRepository(dataSource());
	Person charlie = repository.findPersonByFirstName("Charlie");

	assertThat(charlie.getFirstName(), is("Charlie"));
	assertThat(charlie.getLastName(), is("Brown"));
	assertThat(charlie.getAge(), is(42));
}

A crucial part is missing though: the setup. We need to prepare the database before we can run the test, i.e. we need to make sure the database contains a person called “Charlie” before we can call findPersonByFirstName and check the result.

Importing a dataset into the database

DbUnit offers a useful approach to solving this problem, e.g. it allows to cleanly insert a data set required by a test into the database. Usually, such a data set is specified in a separate XML file, like this:

<dataset>
  <PERSON NAME="Bob" LAST_NAME="Doe" AGE="18"/>
  <PERSON NAME="Alice" LAST_NAME="Foo" AGE="23"/>
  <PERSON NAME="Charlie" LAST_NAME="Brown" AGE="42"/>
</dataset>

This simple dataset contains three rows of the PERSON table which has three columns, namely NAME, LAST_NAME, and AGE.

DbUnit can then read the file into an IDataSet like this:

private IDataSet readDataSet() throws Exception {
	return new FlatXmlDataSetBuilder().build(new File("dataset.xml"));
}

Next, we tell DbUnit to load our IDataSet into the database. In this example, we use an in-memory instance of H2.

private void cleanlyInsertDataset(IDataSet dataSet) throws Exception {
	IDatabaseTester databaseTester = new JdbcDatabaseTester(
		"org.h2.Driver", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", "");
	databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
	databaseTester.setDataSet(dataSet);
	databaseTester.onSetup();
}

The CLEAN_INSERT operation instructs DbUnit to delete all rows from the PERSON table and then insert the rows from our dataset into the database. In our test class, we will import the dataset before each test case using the @Before annotation of JUnit:

@Before
public void importDataSet() throws Exception {
	IDataSet dataSet = readDataSet();
	cleanlyInsertDataset(dataSet);
}

Creating the database schema

However, before we can load the dataset into the database, we have to ensure that the database schema has been created. Since we are using a non-persistent H2 instance we can simply create the schema before running our first test case. In JUnit, this can be achieved using the @BeforeClass annotation like this:

@BeforeClass
public static void createSchema() throws Exception {
	RunScript.execute("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
	                  "sa", "", "schema.sql", UTF8, false);
}

RunScript is an utility class provided by H2 that executes the specified SQL file against the specified database. Other databases provide similiar mechanisms to execute SQL files. In our case, the schema.sql file looks like this:

create table if not exists PERSON (
	ID int identity primary key,
	NAME varchar,
	LAST_NAME varchar,
	AGE  smallint,
)

Putting the pieces together

Our test setup is complete. First, we create the database schema. Second, we import three rows from dataset.xml into the PERSON table. Third, we run our test case.

Here are all the bits and pieces put together:

public class XmlDatabaseTest {

	private static final String JDBC_DRIVER = org.h2.Driver.class.getName();
	private static final String JDBC_URL = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1";
	private static final String USER = "sa";
	private static final String PASSWORD = "";

	@BeforeClass
	public static void createSchema() throws Exception {
		RunScript.execute(JDBC_URL, USER, PASSWORD, "schema.sql", UTF8, false);
	}

	@Before
	public void importDataSet() throws Exception {
		IDataSet dataSet = readDataSet();
		cleanlyInsert(dataSet);
	}

	private IDataSet readDataSet() throws Exception {
		return new FlatXmlDataSetBuilder().build(new File("dataset.xml"));
	}

	private void cleanlyInsert(IDataSet dataSet) throws Exception {
		IDatabaseTester databaseTester = new JdbcDatabaseTester(JDBC_DRIVER, JDBC_URL, USER, PASSWORD);
		databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
		databaseTester.setDataSet(dataSet);
		databaseTester.onSetup();
	}

	@Test
	public void findsAndReadsExistingPersonByFirstName() throws Exception {
		PersonRepository repository = new PersonRepository(dataSource());
		Person charlie = repository.findPersonByFirstName("Charlie");

		assertThat(charlie.getFirstName(), is("Charlie"));
		assertThat(charlie.getLastName(), is("Brown"));
		assertThat(charlie.getAge(), is(42));
	}

	@Test
	public void returnsNullWhenPersonCannotBeFoundByFirstName() throws Exception {
		PersonRepository repository = new PersonRepository(dataSource());
		Person person = repository.findPersonByFirstName("iDoNotExist");

		assertThat(person, is(nullValue()));
	}

	private DataSource dataSource() {
		JdbcDataSource dataSource = new JdbcDataSource();
		dataSource.setURL(JDBC_URL);
		dataSource.setUser(USER);
		dataSource.setPassword(PASSWORD);
		return dataSource;
	}
}

Of course, there’s still some room for improvement. For example, createSchema(), cleanlyInsert(), and dataSource() have yet to be made reusable by other test classes. However, this would destroy the self-containedness of the test which is important for this blog post. ;-)

What’s next?

In the next post, we will see how the test data (now hidden away in dataset.xml) can be moved into the test code. Stay tuned!

blog comments powered by Disqus