London unit test school: test libraries like a boss
In the previous article, we showcased two schools of writing unit tests named from two cities: London and Detroit. They differ in how big (or small) the unit is. Now it’s time to make a deep dive into the first of them. London school assumes that a unit is usually a single class, or a narrow group of closely related classes. Many programmers and projects have been devastated by this technique (including myself!). For this reason, we can find a lot of negative opinions about it. However, there is a nice niche for it: libraries! Let’s see how it works there and why it works there.
tl;dr;
Most libraries already comprise many small, independent classes, which we would like to test separately anyway. In London school, units are very small and focus on very detailed behavior. They do a great job protecting the API contract between the library and its users.
Anatomy of library
Before we start, let’s answer a simple question: what is a library, actually? A typical library has the following features:
- collection of re-usable code: classes, interfaces, functions, constants
- written by someone else*
- little coupling: the units are relatively independent
- no inversion of control: your code calls the library
Regarding point 2… of course, it may happen that you use your own library in your program, because why not? But most of the libraries in your program have been created by someone else. You choose them, so that you don’t have to write this code on your own.
How to understand points 3 and 4? Take a look at Google Guice, a Java library for dependency injection. It’s a library, because it does not do anything on its own. It does not enforce any particular way, how you use it. You can build a web, desktop or console application with it. You can build a modular or monolith app with it. You can use it to control all your dependencies or only a small number of them. You are in charge. It is your task to design the architecture of your application around this library.
Library versus framework
Frameworks are a bit different from libraries. They share the features 1 and 2, but do not have 3 and 4. Instead, they employ a technique called Inversion of Control. The framework provides the architecture, tools and a runtime environment for your program. You only plug in your logic into well-defined places and the framework decides when to call it. Frameworks are much bigger than libraries. The coupling between the components is also higher. Of course, certain components can be used as standalone libraries, but many can not. In this article, we focus on testing libraries, not frameworks.
Have you read the previous article?
If you are not sure what all those “London” and “Detroit” styles mean, feel free to read my previous article “London and Detroit schools of unit tests“, which provides an introduction to the topic.
London school of unit tests in action
Loose coupling and lack of inversion of control are the main reasons why the London style is great for testing libraries. Let’s remind the main feature of the London school:
In the London school of unit tests, most units are small and usually coincide with individual classes or functions. If the coupling is small, you simply cannot make any bigger unit from them that you could test. An attempt to do so would result in creating a lot of plumbing code, which is not a part of your library. Your tests would provide more coverage to this plumbing code rather than the actual library. How would you know if you protected the API contract properly?
London school protects the implementation details
The tests in London style are very detailed, and they “freeze” many of the implementation details of our code. Consider the following code snippet:
interface Initializer {
fun onStart()
fun onRollback()
}
fun initialize(initializers: List<Initializer>) {
val successful = ArrayList<Initializer>(initializers.size)
for (initializer in initializers) {
try {
initializer.onStart()
successful.add(initializer)
} catch (exception: Exception) {
val suppressed = ArrayList<Exception>(successful.size)
for (rolledBack in successful.reversed()) {
try {
rolledBack.onRollback()
} catch (shutdownException: Exception) {
suppressed.add(shutdownException)
}
}
suppressed.forEach(exception::addSuppressed)
throw exception
}
}
}
This code is a simple implementation of a safe initialization procedure. If any of the initializers fails, the others that have already successfully started, have a chance to perform a rollback. What is important, the rollback happens in the reverse order, because the initializers may implicitly depend one on another. Here’s what we should test:
- successful initialization of multiple initializers:
onStart()
called in a specific order,onRollback()
not called - failure of the first initializer:
onStart()
not called for others, noonRollback()
called - failure of the middle initializer:
onStart()
called for the previous initializers, andonRollback()
called for them in the reversed order - failure of the last initializer:
onStart()
called for all initializers,onRollback()
for all but the last one in the reversed order - failure of
onRollback()
itself: exception stored as a suppressed exception, no interruption of callingonRollback()
- … and probably some more
Example test in London style
Let’s see how a test for one of the use cases would look like (Kotest / Kotlin / mockito-kotlin):
should("call onStart for previous initializers and onRollback in reverse order when the middle initializer fails") {
// given
val expectedError = RuntimeException("test")
val initializers = listOf<Initializer>(
mock(),
mock(),
mock {
on { onStart() } doThrow expectedError
},
mock()
)
// when
val exception = shouldThrow<RuntimeException> {
initialize(initializers)
}
// then
exception shouldHaveMessage("test")
inOrder(*initializers.toTypedArray()) {
verify(initializers[0]).onStart()
verify(initializers[1]).onStart()
verify(initializers[2]).onStart()
verify(initializers[1]).onRollback()
verify(initializers[0]).onRollback()
}
verifyNoInteractions(initializers[3])
}
We can see that the test is very detailed. It verifies the code up to the level of the order of function invocations. Many programmers do not like writing tests in this way. Indeed, it does not work well for standalone applications, because it freezes many technical details that often change. For applications, this is a disadvantage, but for libraries… this is exactly what we want.
Let’s notice that in case of our library this behavior is exactly what we “sell”. We provide an initialize()
function that offers a certain behavior. This is our API contract. The users of our library write their own implementations of Initializer
interface, and build their solutions on top of this code. Any unexpected change in the behavior could have large consequences for them. The way how we handle all corner cases also matters. This is why our tests must be so fine-grained.
In short…
In libraries, the small details of the code behavior are exactly what we sell. For this reason our library tests must be so detailed.
London school protects the API backward compatibility
Both Java and Kotlin support type inference. In many cases, we do not have to declare e.g. the variable type, or the returned type, because the compiler can compute it from the remaining code. It simplifies the code, but it is risky in libraries, where every API detail matters. A single innocent commit can accidentally change the function signature. Now imagine that your tests rely on implicit typing. The compiler happily calculates the new types for you, the tests pass. You make a new release and then you start getting a lot of issues. Here are the possible issues:
- compilation errors: the users depend on the old function signature and their code no longer compiles.
- linking errors: the users have another library that depends on the old function signature. That other library cannot load, if we bump our library to the newest version.
For libraries, the best strategy is using 100% explicit types in the main code. The London tests can provide an extra level of protection. Even with explicit typing, changing the function signature is very easy. But unexpected changes in unit tests after the refactoring are a signal for you that this change probably is not safe at all. Here is a simple trick that helps your unit tests protect your function signatures:
// when
val result: Collection<Foo> = testedClass.testedFunction()
If we now try to change the testedFunction()
to return e.g. Set<Foo>
, our test would fail to compile. If we change it through refactoring tools, we have bigger chance to notice it during the code review.
Hey, I’ve looked at your PR. There are some strange changes in “when” sections, could you take a look? It was supposed to be just a bugfix, but it looks like you are trying to change the API.
In short…
When testing a library, always explicitly declare the type of returned values from tested functions.
Conclusion
Many programmers consider London school as a wrong way of writing tests. They point out the high maintenance cost in bigger projects, and large effort to rework existing tests after code changes. If we talk about the application code, these factors are huge disadvantages. But we can see that they fit perfectly the needs of libraries. The tested code is already loosely coupled, so we test individual functions and classes anyway. We also want to test the behavior in great details, because this behavior is exactly what we sell. We don’t expect such tests to change too often, because they prevent us from making breaking changes unintentionally. In other words, if you need to change the tests, your users may be in trouble.
In London school of tests, a unit is usually a single class or function. However, even in libraries there are cases, where we test several classes together. It is not a problem as long as you are confident that your public API contract is sufficiently protected. The reduction in maintenance costs can be worth it. Just beware of the trap of writing tests that test themselves rather than the production code.
Sample code
Find a complete example on Github: zone84-examples/london-unit-tests-example