What is a good unit test?

There are several characteristics of good unit tests. In short, they should prevent regressions, they should be isolated from other components and other tests, they should be repeatable (if the test is isolated, it is also be repeatable), they should be fast and they should be stable, so that changes to the production code don’t require large changes to the tests.

We also need to be aware that we don’t write tests to generate costs, but to reduce them. That way, we’ll be more confident about making changes, so we can release faster, and we won’t slow down as much over the course of the project.

Unit testing approaches

There is no single industry standard approach to unit testing. In projects we see two dominant approaches, London (also known as Mockists) and classical (known as Detroit). Sometimes there is a mixture of those. Sometimes people are more aware of how they write unit tests, sometimes less. In this article, I’ll explain the differences between them, give my opinion and observations.

Key difference - isolation

The source of this division is a difference in the understanding of two terms: unit and isolation.

In the London school, the unit is a class. As a consequence, we threaten most classes (except read-only objects) as external dependencies, so we cannot use them in tests. We have to use test doubles (mocks, stubs, etc) instead.

In the classical school, the unit is a behaviour. A unit test can involve different classes, as long as they don’t depend on components outside the application’s control (such as the file system, databases, etc.). In this way, tests are isolated from each other. By taking this approach, you won’t use test doubles at all when testing the domain model (as the model should not use dependencies from outside the model).

Examples

Let me start by giving an example of how to test in a London school:

test('Customer.withdraw(): should throw an error when not enough money', () => {
	const bankAccount = createMock(BankAccount);
	bankAccount.hasEnoughMoney
		.withArgs(1000)
		.returns(false);
	const customer = new Customer();

	const success = customer.withdraw(2000);

	assert.is(success, false);
	assert.is(bankAccount.addTransaction.calls.length, 0);
});

Note that the test uses a mock to isolate a Customer from a BankAccount. Then, in the assertions section, it validates the communication between the classes.

Now let’s move on to the classical test:

test('Withdraw fails when there is not enough money', () => {
	const bankAccount = new BankAccount({ transactions: [ 1000 ] });
	const customer = new Customer();

	const success = customer.withdraw(2000);

	assert.is(success, false);
	assert.is(bankAccount.balance, 1000);
});

In this test we use a production version of the BankAccount class. In the assertion, instead of validating the communication between classes, we check their state.

Consequences

There are several positive consequences of using the London school approach. As they are fine-grained, they can test very detailed behaviours in a high degree of isolation. They also allow you to interpret the results of the tests accurately. If the test fails, you know exactly where to find the problem. With the classical approach, once a bug has been introduced into production code, you can expect many more tests to fail, making it harder to find the exact cause of the problem.

On the other hand, tests written in the classical school are much more resistant to change. Changing the code will not require as much, or any, change in the tests as in the London school. This makes these tests much cheaper to maintain and gives you more confidence during refactoring. Also, because these tests run more production code, they provide better protection against regressions.

My opinion

In the past I used to choose the London school, but over time it has shifted towards the classical school. I found that the communication between the classes is an implementation detail and doesn’t give us any hint of the business value we want to confirm in the test. So testing in the classical school looks more like documenting a business requirement than it does in the London school.

The other point is that because the London approach is more coupled to implementation, it becomes very time consuming to adapt tests when we change our code. In an ideal world, we always design our code in the best possible way, so we never have to refactor, but that world doesn’t exist, we make mistakes, requirements change, we learn, so there’s always something to improve.

Also, running more production code will always give us better regression protection. Ultimately, the highest level of protection would come from E2E tests, then integration tests, and finally unit tests. But unit tests have (or should have) one advantage that the higher level tests don’t. They are cheap. This is also why I now prefer the classical school - not only are they cheaper than the London school tests, but they also provide better regression protection.

Why I used London approach before

In the end, as I said, I used the London approach more in the past. There were two reasons for this.

It was easier for me to understand that unit = class. It was clear from a technical point of view, but from a business point of view it doesn’t really matter. It’s an implementation detail. Class is something we know and understand as developers, but it’s not important to the customer. What they’re interested in is what the product does, not how it does it.

The other thing is the precision of finding regressions. In the London approach, when you find the failed test, you can tell exactly what changes the regression made, just by looking at the test descriptions. This sounds great, but in practice I haven’t found it as useful as I thought. If you run your tests frequently, you’ll find the regression quickly anyway.

Further reading

I highly recommend a book called “Unit Testing Principles, Practices, and Patterns” by Vladimir Khorikov. It was a great eye-opener for me. Besides detailed comparison of unit testing schools and great guide of unit testing in general, it also covers and organises topics related to integration testing, application architecture (including hexagonal architecture) and domain modelling. I’m pretty sure I’ll be referring to this book a lot in future posts.

Another great recommendation is Martin Fowler’s blog post “Mocks Aren’t Stubs”. As the title suggests, it contains descriptions of various test doubles, but more importantly, he compares the London and Classical schools and gives his observations on how they affect design decisions.