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 mine!). 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 actually works there.

tl;dr;

Usually, libraries already comprise many small, independent classes, that we would like to test separately anyway. In London school, units are very small and we focus on very detailed behavior. It works well if we use it to protect 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? I would say that libraries have the following features:

  1. collection of re-usable code: classes, interfaces, functions, constants
  2. written by someone else*
  3. little coupling: the units are relatively independent
  4. 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 usually, most of the libraries in your program have been created by third parties. You choose them, so that you don’t have to write much of the 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 with this library.

Library versus framework

Frameworks are a bit different from libraries. They are similar in points 1 and 2, but do not share points 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 others cannot. It’s the decision of the framework authors. 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 are about, feel free to read my previous article “London and Detroit schools of unit tests” that provides an introduction to the topic.

London school of unit tests in action

Little coupling and lack of inversion of control are the main reasons why we can write unit tests for our libraries in London style. Let’s remind the main feature of the London school:

London versus Detroit school of unit tests

Yes, in London school of unit tests, the units are small and usually coincide with individual classes or even functions. If the coupling is small, you can’t even make any bigger unit from them that you could test. Why? Because it would comprise mostly of the plumbing code which is not a part of your library. And your tests would mostly cover that plumbing code, not your logic. In this case, can you tell that you sufficiently verified the API contract?

London school protects the implementation details

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, all the initializers 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:

  1. successful initialization of multiple initializers: onStart() called in a specific order, onRollback() not called
  2. failure of the first initializer: onStart() not called for others, no onRollback() called
  3. failure of the middle initializer: onStart() called for the previous initializers, and onRollback() called for them in the reversed order
  4. failure of the last initializer: onStart() called for all initializers, onRollback() for all but the last one in the reversed order
  5. failure of onRollback() itself: exception stored as a suppressed exception, no interruption of calling onRollback()
  6. … 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 checks the code up to the level of the order of function invocations. Many programmers don’t like writing tests in this way. Indeed, it does not work well for standalone applications, because it freezes many technical details that may change often. 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 certain behavior. This behavior is our API contract. The users of our library write their own implementations of Initializer interface, and build their solution upon our code. They explicitly or implicitly depend on this behavior, and we must be very explicit about how it works. This is why our tests must be so fine-grained, too. We simply cannot change the behavior every two versions – we need to protect it from changing.

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 don’t have to declare e.g. the variable type, or the returned type, because the compiler can compute it from the remaining code. It can simplify the code, however, it is a risky technique for libraries. By making innocent changes in our code, we can accidentally change e.g. the return type of a function, and miss it. If all our tests and implementation rely on implicit typing, the compiler would calculate the new types and everything would seem to work. Unfortunately, later our users would complain:

  • some of them relied on the previous return type, therefore their project would not compile after updating the version of our library,
  • they use another library dependent on our library, and it turns out that they get linker errors: that second library is compiled against the OLD function signatures from our code and simply cannot work, if we bump the dependency.

The simplest solution is making all argument and return types 100% explicit in our main code. We can additionally use the detailed tests in London style to provide an extra level of protection. Still, changing a function signature is easy, especially if nothing prevents us from doing so. But compilation errors in unit tests are a signal for us: dude, if you need to update your unit tests, probably this change is not safe. Below, we can see a simple trick to enforce certain types:

// 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.

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 use case of testing code 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, it means that your users will cry.

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’s not a problem as long as you are confident that your public API contract is sufficiently protected, and that it helps you reducing the maintenance cost. Just avoid 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

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments