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.

Prefer Classical over London style when writing tests

Next
Next

Unit vs Integration vs Functional Testing