(Coursenotes for CSC 305 Individual Software Design and Development)

Designing for testability

Sources

Outline

flowchart LR

A --> B
B --> db[(Database)]
B --> m[[Many other dependencies]]
A --> C

In the above example, if want to test the A module, would also need instances of modules B and C, which would require their dependencies to initialised, and so on. This is a lot of work to just test the module we wanted to test in the first place!

Testing and dependency injection

A common strategy is to create fake B and C objects and inject them into A for the purpose of a given test. The fake B and C modules are given “hard-coded” behaviour. We’re pretending that they work as expected in the context of a particular test case, so we can focus our assertions on the behaviour of module A.

Some advantages of this are:

In the code below, all instantiations of A would require “full” or “real” instantiations of B and C objects.

public class A() {
  private B b;
  private C c;

  public A() {
    this.b = new B();
    this.c = new C();
  }
}

But if we were to use dependency injection:

public class A() {
  private B b;
  private C c;

  public A(B b, C, c) {
    this.b = b;
    this.c = c;
  }
}

Now in our tests, we can initialise our A object like this:

new A(new FakeB(), new FakeC())

That’s the high-level idea.

Test doubles

Test doubles are objects used to stub out parts of a system that are not the focus of tests, but are “collaborators” or dependencies of the part that you are testing.

There are a number of different types of test doubles. I list them below for the sake of completeness, but we’ll talk about two in some more detail.

The final two categories are stubs and mocks. These are the ones we’ll talk about in a bit more detail.

For example, suppose you are creating a system to manage invoices for a store. You have a method called getAllInvoices in the application. The method is supposed to return all the invoices from the database, which requires connecting to a database server that could potentially require an internet connection.

And suppose you’ve got some class that happens to call the getAllInvoices method, and you’re interested in testing what happens next. That is, the getting of all the invoices is not the focus of your testing.

You can create a stub of the getAllInvoicesMethod that will return a pre-defined hard-coded list of invoices, used for this test. That is, you’re just assuming that the getAllInvoices method already works as expected.

This is a common strategy, because often all you need from a dependency is for it to return a value that your system-under-test will use.

The categories above are useful conceptualisations, but it’s more useful to think of them as the role of the test double rather than the type of the test double.

Mockito is a popular mocking and stubbing library for Java.

It kind of combines many of the concepts above into one — you can create a mocked object, and

The examples below are from the Mockito website (I couldn’t find a way to directly embed the code snippets):

Stubbing method calls

We create a dummy list and then give it a hard-coded response to the method call get(0).

import static org.mockito.Mockito.*;

// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);
// or even simpler with Mockito 4.10.0+
// LinkedList mockedList = mock();

// stubbing appears before the actual execution
when(mockedList.get(0)).thenReturn("first");

// the following prints "first"
System.out.println(mockedList.get(0));

// the following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

Using Mock objects and verifying interactions with the mock object

We create a dummy list and then verify that it was used in exactly the way we expected. E.g., add("one") was called once, and clear() was called once.

Remember that this list itself is not the focus of your test. The focus of your test (the “system under test” or SUT) is the thing that’s using the list. So what you’re interested in is how that SUT interacts with the list.

import static org.mockito.Mockito.*;

// mock creation
List mockedList = mock(List.class);
// or even simpler with Mockito 4.10.0+
// List mockedList = mock();

// using mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one");
mockedList.clear();

// selective, explicit, highly readable verification
verify(mockedList).add("one");
verify(mockedList).clear();

Example

The author of Effective Software Testing, Maurício Aniche, has kindly made all his code examples available on GitHub. We’ll look at a quick example of using Mockito to stub out a dependency. Specifically, we are looking at the following:

Key things to look at in the source code

If we wanted to test the InvoiceFilter class, we would need to initialise a DatabaseConnection object and use that to initialise an IssuedInvoices object. You see that in the InvoiceFilterWithDatabaseTest.java file. Before each test case, a new database connection must be established (and reset so that each test gets a fresh DB).

Now imagine a larger class with a more complex (i.e., more realistic) database schema. And recall our discussion about systematically choosing a thorough (“requirements covering”) set of test inputs. Our tests would get untenably complicated to setup, and untenably slow to run. All because we have to initialise and setup a dependency (DatabaseConnection) that’s not the focus of our testing in the first place!

Using a stub object. Now take a look at InvoiceFilter. The constructor takes a parameter IssuedInvoices, which it uses to initialise the dependency in the class. Now, a client using the class has to inject an IssuedInvoices object into the InvoiceFilter class. In this case, the “client” that’s using the module is our tests. As a result of this change, the InvoiceFilter has no need for a DatabaseConnection dependency, and we can simplify away large swathes of that behaviour.

Take a look at InvoiceFilterTest. The amount of code you’re writing is more or less the same, but without the slow DB connection and without persisting information to a database that needs to be cleared between test runs. Also, crucially, the stubbing away of the database connection means that bugs there wouldn’t affect these tests. We are implicitly assuming here that the database connection has been tested in isolation as well.

To mock or not to mock? That is the question

Mocking makes your tests less realistic. Your tests are relying on some imaginary object that will never exist in real usage of your system.

The lack of coupling between modules that you are afforded by your mock objects can actually lead you astray.

For example, suppose you have modules A and B. In the tests for A, you have mocked the behaviour of B. In some future change, the postconditions assured by B change due to changes in your requirements. Typically, when you make changes to B, you would also update tests for B.

But it would be easy to forget to check if A handles these changes well. Your tests for A would pass with no problems, because they are relying on an “assumed good” version of B that is now out of date.

Like all tools, there are specific good times to use mocking:

You should NOT mock types that you don’t have control over (e.g., from external libraries). If that library ever changes, your tests would happily continue passing, because they rely on an idealised version of it.

When you use a mocking library to create a dummy version of some dependency, the fundamental assumption you’re making is that the dependency is itself being tested elsewhere. If you create mocks of objects that you don’t “own”, then you can no longer make this assumption.

Designing for testability

Many rules about software design in general also apply to designing for testability. So I won’t repeat them in too much detail.

We talk about “designing for testability” (i.e., designing so that testing is easier), but we can also think about things in the opposite direction. If testing difficulties arise like the ones below, that is often good feedback for you about your software’s design.


  1. The book is great and I highly recommend it.