Unit Tests Should Only Break If Class Under Test Breaks
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:
FakeWebsocketServeris a fake WebsocketServer implementation.- It has some fake implementations and some mocked.
- The events
on_client_connected,on_client_disconnected, andon_message_receivedare 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
broadcastmethod 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.