DbUnit Best Practices: Writing Assertions for the Data Found From Our Database
After we have finished this lesson, we:
- Understand why we shouldn’t write our assertions by using only one DbUnit data set.
- Can identify the requirements of clean DbUnit data sets.
- Know how we can create DbUnit data sets which test only one logical concept.
This lesson assumes that:
The Most Common Mistake Found From DbUnit Data Sets
One quite common pattern is that developers write their assertions by using only one data set per test case. This data set specifies the data that should be found from the database after the test case has been run.
For example, if we want to verify that an open task was saved to our database, we could use a DbUnit data set that looks as follows:
1<dataset>
2 <persons id="1"
3 email_address="john.doe@gmail.com"
4 name="John Doe"
5 password="password"
6 user_role="ROLE_USER"/>
7 <tasks id="1"
8 assignee_id="[null]"
9 closer_id="[null]"
10 creator_id="1"
11 description="bar"
12 resolution="[null]"
13 status="OPEN"
14 title="foo"/>
15 <tags/>
16 <tasks_tags/>
17</dataset>
[null] is replaced with a null value when DbUnit compares the content of our database with the values specified in our data set.
I have to admit that this data set doesn’t look so bad because it has so few rows. However, typically the data sets which use this approach suffer from these problems:
- They contain irrelevant data. This is a problem because unnecessary assertions make our test slower than it could be AND more importantly, irrelevant data makes our data sets hard to read.
- They hide the essence of our test case. I don’t know if you have noticed this, but quite often these tests are called
shouldSaveXToDatabase(). The reason for this is that our data set tries to write too many assertions, and that is why it is impossible to figure out a good name for our test case.
It’s clear that when we are writing assertions for the data that is found from our database, we simply cannot verify every column value of every database table.
Let’s move on and find out how we can write better assertions for the data found from our database.
Writing Assertions That Don’t Suck
I think that a good DbUnit data set fulfills these three requirements:
- It helps us to identify the relevant database tables. A database table is relevant for our test case if its content can be changed by the tested code or its content is mentioned in the business requirements of the tested feature.
- It tests only one logical concept. A DbUnit data set is essentially a list of assertions that are specified in XML format. If we think that a test method cannot have too many assertions, it is clear that this must be true for our DbUnit data set as well.
- It helps us to figure out what is wrong if our test case fails. A good test case is written for humans to read, and this is true for our DbUnit data sets as well.
These requirements raise an obvious question:
How can we create DbUnit data sets that fulfill these requirements?
Let’s find out an answer to that question by modifying our existing DbUnit data set which verifies that the create new task function is saving the correct information to our database.
As we remember, the source code of our original DbUnit data set looks as follows:
1<dataset>
2 <persons id="1"
3 email_address="john.doe@gmail.com"
4 name="John Doe"
5 password="password"
6 user_role="ROLE_USER"/>
7 <tasks id="1"
8 assignee_id="[null]"
9 closer_id="[null]"
10 creator_id="1"
11 description="bar"
12 resolution="[null]"
13 status="OPEN"
14 title="foo"/>
15 <tags/>
16 <tasks_tags/>
17</dataset>
We have to split this data set into multiple smaller data sets which fulfill the requirements of good DbUnit data sets. We can do this by following these steps:
First, we have to remove irrelevant database tables from our data set. Because the tested code cannot make any changes to the persons table, we don’t have to verify its content in our data set.
After we have removed the persons table from our data set, our data set looks as follows:
1<dataset>
2 <tasks id="1"
3 assignee_id="[null]"
4 closer_id="[null]"
5 creator_id="1"
6 description="bar"
7 resolution="[null]"
8 status="OPEN"
9 title="foo"/>
10 <tags/>
11 <tasks_tags/>
12</dataset>
Second, we have to split our data set into smaller data sets and ensure that each data set tests only one logical concept.
Let’s go through our new data sets one by one:
The data set, which verifies that the created task has no tags, looks as follows:
1<dataset>
2 <tags/>
3 <tasks_tags/>
4</dataset>
The data set, which verifies that creator of the saved task is correct, looks as follows:
1<dataset>
2 <tasks id="1"
3 creator_id="1"/>
4</dataset>
The data set, which verifies that the saved task has the correct title and description, looks as follows:
1<dataset>
2 <tasks id="1"
3 description="bar"
4 title="foo"/>
5</dataset>
The data set, which verifies that an open task was saved to the database, looks as follows:
1<dataset>
2 <tasks id="1"
3 closer_id="[null]"
4 resolution="[null]"
5 status="OPEN"/>
6</dataset>
The data set, which verifies that the saved task has not been assigned to anyone, looks as follows:
1<dataset>
2 <tasks id="1"
3 assignee_id="[null]"/>
4</dataset>
If we compare our old data set with our new data sets, we can see that:
- Our new data sets are easier to read than our old data set because they are a lot smaller and don’t contain irrelevant information.
- Because each data set concentrates on testing one logical concept, these data sets help us to write test methods that highlight the business requirements of the tested feature.
- If we use our new data sets, we have to write five integration tests instead of one test. Most of the time this is not a problem, but we have to understand that this is one of those trade-offs that we have to make.
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- If we use only one data set per test case, we cannot write integration tests that emphasize our business requirements.
- A good DbUnit data set doesn’t contain irrelevant data and it tests only one logical concept.
- Sometimes we have to make a trade-off between a super fast test suite and clean DbUnit data sets.