This’ll be a short one.

BLUF: Unit Tests Should Only Break If Class Under Test Breaks

Unit tests should only break if the class under test breaks, not if one of its dependencies breaks.

When you see a unit test failing, assume that the code/logic of the class under test is failing, not the code/logic of one of its dependencies (assuming the unit tests were witten properly).

How do you ensure that your unit test only fails if the class under test fails? Simple: mock/fake its dependencies.

How you should mock/fake (and which you should use) depends on that particular test case. Assume a particular test case and a particular dependency.

Class to Dependency Communication

If in that particular test case, and for that particular dependency, communication is only from the class under test to the the dependency, i.e., the class under test calls the dependency’s methods, not the other way around, use a mock. Mock the dependency and assert the correct methods were called with the correct type/values. Simple.

Dependency to Class Communication

If the dependency needs to communicate with the class under test, use a fake.

A fake is a lightweight implementation of the dependency that behaves like the real one but is easier to control and set up for testing. It also only implements methods/functionality that is needed for that test case!

Example

In python, you can easily create fakes that have certain attributes mocked! This allows creating fakes extremely easily.

import pytest
from unittest.mock import MagicMock

class Publisher:
    def __init__(self):
        self.subscribers = []

    def subscribe(self, subscriber):
        self.subscribers.append(subscriber)

    def notify(self, message, *args, **kwargs):
        for subscriber in self.subscribers:
            subscriber(message, *args, **kwargs)

class FakeWebsocketServer:
    def __init__(self):
        self.on_client_connected = Publisher() # faked
        self.on_client_disconnected = Publisher() # faked
        self.on_message_received = Publisher() # faked

        self.broadcast = MagicMock() # mocked

def test_lockstep_server_sends_welcome_message_when_client_connects():
    websocket = FakeWebsocketServer()
    lockstep = LockstepServer(websocket)

    # simulate client connecting ("message" going from dependency to class under test)
    websocket.on_client_connected.notify("client1")

    # assert that the lockstep server sent a welcome message
    websocket.broadcast.assert_called_once_with("Welcome client1!")

In the above example:

  • FakeWebsocketServer is a fake WebsocketServer implementation.
  • It has some fake implementations and some mocked.
  • The events on_client_connected, on_client_disconnected, and on_message_received are faked, because our class under test listens to these events, so they need actual subscribe methods and when we emit events on them, we need to ensure the class under test receives the emitted events.
  • The broadcast method is mocked, because we just have to ensure this is called.

Integration Tests

To reiterate, unit tests should only break if the class under test breaks, not one of its dependencies.

If you want tests that check whether multiple classes work together correctly, write integration tests.

Generally keep these seperate from your unit tests, something like:

/src
/tests
    /unit
    /integration

Summary

  • Unit tests should only break if the class under test breaks.
  • Mock dependencies when the class under test only calls their methods.
  • Use fakes when the dependency needs to call back into the class under test.