Unit Testing: London vs Classical
Broadly, a unit test accomplishes 3 things
verifies a small piece of code (a unit)
does it quickly
does it in an isolated manner
Within the world of unit testing, two schools of thought have emerged with different ways to interpret what isolation means.
The London school of testing interprets isolation to mean the system under test should be isolated from its dependencies. As a result, code bases subscribing to the London school of thought make heavy use of mocks to simulate the interaction of the system under test with other objects.
The classical school of testing interprets isolation to mean the tests should operate in isolation from each other. In these code bases mocks are still used, but instead of mocking other objects within the code base, only shared dependencies are mocked.
The different interpretations of isolation lead to different ways to interpret a “unit.” Within the London paradigm, a unit is a class or function, while within the classical school the unit can still be an individual class or function, but it could also be a set of interacting objects as long as no dependencies exist between tests. Take the following code for instance
def calculate_total_balance(x, y):
# complex business logic
return some_calculator_object.sum(x, y)
In the London school of thought some_calculator_object.sum() is a dependency, and must be mocked
import pytest
from unittest.mock import Mock
from your_module import calculate_total_balance
@pytest.fixture
def mock_calculator():
return Mock()
def test_calculate_total_balance(mock_calculator):
x = 10
y = 20
mock_calculator.sum.return_value = 30 # Mocking the sum method
result = calculate_total_balance(x, y)
assert result == 30 # Verify the result
# Check if the sum method was called with the correct arguments
mock_calculator.sum.assert_called_once_with(x, y)
Here is the same test, rewritten in the classical style
import pytest
from your_module import calculate_total_balance
def test_calculate_total_balance():
x = 10
y = 20
result = calculate_total_balance(x, y)
assert result == 30 # Verify the result
London School Benefits
When a test fails, you know exactly where something broke in the code
Easier to test a complex web of functions/objects
London School Costs
Over-specification: Tests are more tightly coupled to implementation, making refactoring difficult and tests brittle.
Classical School Benefits
Greater coverage from a single test
Easier to refactor code under test without breaking test
Classical School Costs
Identifying the specific code change that caused a test fail is less straightforward
Discussion
Lets reiterate what makes a good test and evaluate these two styles against that metric:
A good test validates intended behavior of code, while minimizing:
refactoring required when code under test is changed
time spent running the test
dealing with false alarms raised by test
time spent reading the test to understand its purpose
Given these metrics, the time spent running the tests should be about equivalent between the two styles. In my opinion, all the additional mocking from the London school makes understanding what is being tested less straightforward, but ultimately lets say that difference is also marginal. This leaves refactoring the test when the code under test changes, and dealing with false alarms raised by a test. In these two instances the clear advantage goes to the classical school. The looser coupling between the test and the implementation makes changes to the system under test require less refactoring, and the necessity to change the test when the code changes actually invites false alarms, building that cost into the style.