Growing Object-Oriented Software: Guided by Tests is the best book I’ve ever read about testing.

Because it’s not about testing.

Yes, the book talks a lot about clean and readable tests. But tests are just a tool to guide you to create good object oriented software.

The authors walk you through the principles of well-designed object-oriented software and, incidentally, how tests can help you achieve that.

In this post, I want to share what I learnt in Growing Object-Oriented Software: Guided by Tests about testing persistence and how to apply it to the Android ecosystem.

Persistence refers to the ability of an object to survive the process that created it. One way to achieve that goal is by saving or persisting its state in a database.

When dealing with persistence, you will usually have to rely on third-party code to do the heavy lifting for you. A critical point about third-party code is that you don't own it. You need to take it as you find it and focus on the integration between your system and the external code.

When integrating with a third party database API (eg Android SQLiteDatabase ) you need to test that your implementation does all these things correctly:

Sends correct queries

Maps correctly between objects and the database schema

Performs updates and deletes that are compatible with database integrity constraints

Interacts correctly with the database transaction manager

Releases external resources correctly

etc...

When testing persistence you need to pay extra attention to test quality. There are several components involved that your tests need to set up correctly. And there is always a persistent state that can make your tests interfere with each other.

This example uses a simple TODO app, specifically exploring its persistence layer.

A user can create tasks with a name and an expiration date. The task list can be filtered so only non-expired tasks are displayed.

The persistence layer of the system is represented in the figure below.

Isolate tests that affect persistent state

Persistent data stays around from one test to the next, so you need to take extra care to ensure persistence tests are isolated from one another.

In database tests this means deleting rows before a test starts.

This cleaning process will depend on the database's integrity constraints (foreign keys, cascade deletes, etc.)

Cleaning persistent data on test start and not on test finish has two main advantages:

Test data remains after a test finish so it’s easier to diagnose failures.

Returning to initial state after testing can be risky. It can lead to tests not doing anything at all and still passing.

Database cleaning constraints, like tables delete order, should be captured in one single place, since the database scheme tends to evolve.

You can use DatabaseCleaner to group those constraints. In this example it only contains one table, but you could add more in a real implementation.

public class DatabaseCleaner { private static final String[] TABLES = { // Add tables to delete here TaskEntry.TABLE_NAME }; private final TaskReaderDbHelper dbHelper; public DatabaseCleaner(TaskReaderDbHelper dbHelper) { this.dbHelper = dbHelper; } public void clean() throws SQLException { SQLiteDatabase sqLiteDatabase = dbHelper.getWritableDatabase(); for (String table : TABLES) { sqLiteDatabase.delete(table, null, null); } dbHelper.close(); } }

It uses a list of tables to ensure correct cleaning order.

Now you can add this to your test suite setup method to clean up the database before each test:

@RunWith(AndroidJUnit4.class) public class ExamplePersistenceTest { @Before public void setUp() throws Exception { [...] DatabaseCleaner cleaner = new DatabaseCleaner(dbHelper); cleaner.clean(); } [...] }

Do commit!

A common testing technique to isolate tests is to run each test in a transaction and roll it back at the end of the test.

The problem with this technique is that it doesn't check what happens on commit.

A database checks integrity constraints on commit. A test that never commits won't be fully checking how the class under test interacts with the database. This committed data could also be useful for diagnosing failures.

So tests should make it obvious when transactions happen.

In this case, every call to TaskRepository.persistTask() will commit to the database directly. That is our transaction boundary.

Testing an object that performs persistence operations

You can use the above previous setup to start testing objects that persist data in a database.

In our domain model, a TaskRepository represents all the operations you can perform around saving or reading: you can add new tasks to the database, find tasks by name and find tasks that are already expired.

public class TaskRepository { [...] public void persist(Task task) {...} public List<Task> tasksExpiredBy(Date date) {...} public Task taskWithName(String taskName) {...} }

TaskRepository has two collaborators:

TaskStorage which does the low level communication with the Android SQLite database.

which does the low level communication with the Android SQLite database. TaskMapper which converters from domain model objects to database objects.

When unit-testing code that uses a TaskRepository as collaborator you can mock the interface directly. There is no need for real database access.

Classes using TaskRepository to persist and query tasks need to trust their collaborators to work correctly. It is the collaborator responsibility to deal with a real database but, from an external point of view, TaskRepository just returns tasks stored somewhere.

However, when testing TaskRepository , you need to be sure it queries and maps objects into the database correctly. In the test below we exercise the tasksExpiredBy method:

@RunWith(AndroidJUnit4.class) public class TaskRepositoryTest { private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); private TaskRepository taskRepository; @Before public void setUp() throws Exception { Context appContext = InstrumentationRegistry.getTargetContext(); TaskReaderDbHelper dbHelper = new TaskReaderDbHelper(appContext); TaskDBStorage storage = new TaskDBStorage(dbHelper); TaskMapper mapper = new TaskMapper(); taskRepository = new TaskRepository(mapper, storage); DatabaseCleaner cleaner = new DatabaseCleaner(dbHelper); cleaner.clean(); } @Test public void findsExpiredTasks() throws Exception { String deadline = "2017-01-14"; addTasks( aTask().withName("Task 1 (-Valid-)").withExpirationDate("2017-01-31"), aTask().withName("Task 2 (Expired)").withExpirationDate("2017-01-01"), aTask().withName("Task 3 (-Valid-)").withExpirationDate("2017-02-11"), aTask().withName("Task 4 (-Valid-)").withExpirationDate("2017-02-14"), aTask().withName("Task 5 (Expired)").withExpirationDate("2017-01-13") ); assertTasksExpiringOn(deadline, containsInAnyOrder( aTaskNamed("Task 2 (Expired)"), aTaskNamed("Task 5 (Expired)")) ); } private void addTasks(final TaskBuilder... tasks) { for (TaskBuilder task : tasks) { taskRepository.persist(task.build()); } } private void assertTasksExpiringOn(String deadline, Matcher<Iterable<? extends Task>> taskMatcher) throws ParseException { Date date = dateFormat.parse(deadline); assertThat(taskRepository.tasksExpiredBy(date), taskMatcher); } }

Interesting things happening in this test:

addTasks method receives a builder to set up name and task expiration date.

method receives a builder to set up name and task expiration date. The expiration date is the most significant field for this test, so you can create tasks with different dates around the deadline to test boundary conditions.

Each task name is self-describing to easily identify instances in case of failure.

assertTasksExpiringOn runs the query you are testing and checks the results.

runs the query you are testing and checks the results. containsInAnyOrder returns a matcher that checks for elements in a collection.

returns a matcher that checks for elements in a collection. aTaskNamed is a custom matcher that checks whether an object is a Task with a given name.

This test implicitly exercises TaskRepository.persistTask when setting up the database for the query.

The relationship between adding a task and querying that task is something that matters to the app domain logic, so you shouldn’t need to test persistTask independently.

(If there is an effect on the system by persistTask that is not checkable using tasksExpiredBy then you have bigger problems you will need to address before considering adding a separate test for persistTask )

This test shows a very nice example of using custom matchers for better test structure and readability. So, why not learn how to create your own?

Here is an implementation of TaskRepository that passes the test:

public class TaskRepository { private final TaskMapper taskMapper; private final TaskStorage taskDBStorage; public TaskRepository(TaskMapper taskMapper, TaskDBStorage taskDBStorage) { this.taskMapper = taskMapper; this.taskDBStorage = taskDBStorage; } public void persistTask(Task task) { TaskDBModel taskDBModel = taskMapper.fromDomain(task); taskDBStorage.insert(taskDBModel); } public List<Task> tasksExpiredBy(Date date) { long expirationDate = date.getTime(); return dbTasksToDomain(taskDBStorage.findAllExpiredBy(expirationDate)); } public Task taskWithName(String taskName) { TaskDBModel taskDBModel = taskDBStorage.findByName(taskName); return taskMapper.toDomain(taskDBModel); } @NonNull private List<Task> dbTasksToDomain(List<TaskDBModel> allExpiredBy) { List<Task> tasks = new ArrayList<>(); for (TaskDBModel taskDBModel : allExpiredBy) { tasks.add(taskMapper.toDomain(taskDBModel)); } return tasks; } }

Testing mappings

Round-trip tests check that the mappings to and from the database are configured correctly.

Mappings can be defined by code or configuration, and errors on those are very difficult to diagnose.

You need to round-trip all the possible object types that could be persisted in the database. For that, you can use a list of test data builders for those types.

You can use these builders more than once, with different setups, to create round-tripping entities in different states.

This test goes through the list of builders, creates and persists an entity in one transaction and retrieves and compares the result in another one.

@RunWith(AndroidJUnit4.class) public class PersistabilityTest { List<TestBuilder<Task>> persistentObjectBuilders = Arrays.<TestBuilder<Task>>asList( // Add different Task configurations here aTask().withName("A task").withExpirationDate("2017-03-16"), aTask().withName("A task (with date in the past)").withExpirationDate("2017-01-01") ); @Before public void setUp() throws Exception { [...] // Same as before } @Test public void roundTripsPersistentObjects() { for (TestBuilder builder : persistentObjectBuilders) { assertCanBePersisted(builder); } } private void assertCanBePersisted(TestBuilder<Task> builder) { assertReloadsWithSameStateAs(persistedObjectFrom(builder)); } private void assertReloadsWithSameStateAs(Task original) { Task savedTask = taskRepository.taskWithName(original.getName()); assertThat(savedTask, equalTo(original)); } private Task persistedObjectFrom(TestBuilder<Task> builder) { Task original = builder.build(); taskRepository.persistTask(original); return original; } }

persistedObjectFrom asks its given builder to create an entity and persists it. Then it returns that entity for later comparison.

asks its given builder to create an entity and persists it. Then it returns that entity for later comparison. assertReloadsWithSameStateAs retrieves the given entity from the database using its name and calls a matcher to check if the two copies are the same.

Round-tripping related entities

Things get complicated when there are relationships between entities. Database constraints are violated if you try to save an entity without related existing data.

To fix this you need to make sure that related data exists in the database before saving an entity for a round-trip test.

For example, let’s say you were to add Lists to your model so each Task belongs to a List .

Your Task round-trip tests will need to change to insert a dummy List before inserting tasks in the database. A good way to do that is to delegate the creation of related data to another builder. The builder of the entity under test ( Task ) will use the builder of the other entity it depends on ( List ) to persist the related data (a dummy list) before the entity under test is persisted.

You could change your tests to include a ListBuilder decorated like this:

private TestBuilder<List> persisted(final TestBuilder<List> listBuilder) { return new TestBuilder<List>() { @Override public List build() { List list = listBuilder.build(); listRepository.persistList(list); return list; } }; }

See more about round-tripping related entities in this test.

Conclusion

Persistence tests are more complicated than regular ones. They require more setup and scaffolding making them difficult to read, maintain and evolve.

But don’t let that put you off. There are two main things to remember:

Test whether the find queries return the data they are supposed to return (this will test insertions as well).

queries return the data they are supposed to return (this will test insertions as well). Test whether the data mappings convert the persistence model to the domain model correctly.

Covering those will give you a high level of confidence that your persistence logic is working correctly. You will have unit tests for the individual components, like mappers, and integration tests to make sure the whole abstraction built for persistence is working correctly.

And by following the ideas and principles described in this post, like custom matchers, your tests will stay nice and clean.

A working example can be found here.