Understanding the Two Schools of Unit Testing
This article provides an overview of Classical school and London school of unit testing in an attempt to understand whether any one of them is better.
Join the DZone community and get the full member experience.
Join For FreeUnit testing is an essential part of software development. Unit tests help to check the correctness of newly written logic as well as prevent a system from regression by testing old logic every time (preferably with every build). However, there are two different approaches (or schools) to writing unit tests: Classical (a.k.a Detroit) and Mockists (or London) schools of unit testing.
In this article, we’ll explore these two schools, compare their methodologies, and analyze their pros and cons. By the end, you should have a clearer understanding of which approach might work best for your needs.
What Is a Unit Test?
A unit test checks whether a small piece of code in an application works as expected. It isolates the tested block of code from other code and executes quickly to identify bugs early.
The primary difference between Classical school and London school is in their definition of isolation. London School defines isolation as the isolation of a system under test (SUT) from its dependencies. Any external dependencies, such as other classes, are replaced with test doubles (e.g., mocks or stubs) to ensure the SUT’s behavior is unaffected by external factors.
The Classical school focuses on isolating tests from one another, enabling them to run independently and in parallel. Dependencies are tested together, provided they don’t rely on shared states like a database, which could cause interference.
Another important difference between the two approaches lies in the definition of what a unit is. In the London approach, a unit is usually a single class or a method of a class since all the other dependencies are mocked. Classical school can test a piece of logic consisting of several classes because it checks a unit of behavior but not a unit of code. A unit of behavior here is something useful for the domain, for example, API of making a purchase without atomizing it on smaller actions such as withdrawal and deposit and testing them separately.
A Comparison of the Two Schools
Here are two examples of a test written in Classical and London styles.
Here is a test written in Classical style:
@Test
public void withdrawal_success() {
final BankAccount account = new BankAccount(100);
final Client client = new Client();
client .withdraw(20, account);
assertThat(account.getBalance()).isEqualTo(80);
}
And here is the same test but written in London style:
@Test
public void withdrawal_success() {
final Client client = new Client();
final BankAccount accountMock = mock(BankAccount.class);
when(accountMock.isSufficient(20)).thenReturn(true);
client .withdraw(20, account);
verify(accountMock, times(1)).withdraw(20);
}
The difference between the two examples is in BankAccount
object. In the Classical approach, the test uses a real BankAccount
object and validates the final state of the object (the updated balance). In the London approach, we had to define the exact behavior of the mock to satisfy our test. In the end, we verified that a certain method was called instead of checking the real state of the object.
Key Differences
Testing of Implementation vs. Testing of Abstractions
London School
The London approach leads to highly detailed tests. It happens because with this approach a test contains implementation details that are hardcoded and always expected to be as they were described in the test. This leads to the vulnerability of tests. Any time one makes a change to some inner logic, tests fail. It happens even if it doesn’t result in changing the output of the test (e.g., splitting the class into two). After that, one has to fix broken tests, and this exercise doesn’t lead to a higher product quality, nor does it highlight a bug. It is just an overhead one has to deal with because the tests are vulnerable.
Classical School
The classical approach doesn’t have this problem, as it checks only the correctness of the contract. It doesn’t check whether some intermediate dependencies were called and how many times they were called. As a result, if a change was made in the code that didn’t cause a different output, tests will not fail.
Bugs Localization
London School
If you made a bug, you would be able to quickly identify the problem with the London testing approach, as usually, only relevant tests would fail.
Classical School
On the other hand, in Classical style one would see more failed tests because they may check the callers of a faulty class. This makes it harder to detect the problem and requires extra time for debugging. This is not a big problem, however. If you run tests periodically, you will always know what caused a bug. In addition, one doesn’t have to check all the failed tests. Fixing a bug in one place usually leads to fixing the rest of the tests.
Handling Complex Dependency Graphs
London School
If you have a large graph of dependencies, mocks in the London approach are very helpful to reduce the complexity of preparing tests. One can mock only the first level of dependencies without going deeper into the graph.
Classical School
In the Classical approach, you have to implement all the dependencies, which may be time-consuming and take effort. On the other hand, a deep graph of dependencies can be a good marker of the poor design of an application. In this case, tests can only help identify flaws in the design.
Integration Tests
The definition of an integration test varies between the two schools.
London School
In the London style of testing, any test with implemented (not mocked) dependency is an integration test.
Classical School
The majority of unit tests in the Classical style would be considered integration tests in the London approach.
Conclusion
Both schools have their pros and cons. In my opinion, Classical school is preferable because it does not have a problem with test vulnerability as in London school. However, the London or mockist style is actively used and very popular, likely due to tools that set up certain ways of testing, for example, JUnit + Mockito for Java apps.
Ultimately, the choice depends on your project’s needs and the trade-offs you’re willing to make.
Opinions expressed by DZone contributors are their own.
Comments