Testing heuristics when it comes to mocks and fakes and contract verification
04-09-2025
-
my heuristics
- for all tests
- you want corrrectness and speed
- for routers
- ensure the signature is valid (ie the underlying methods and attributes are called correctly)
- for clients (external APIs)
- use mocks and autospec
- ensure the signature is valid (ie the underlying methods and attributes are called correctly)
- for clients (databases)
- use sqlite in the session manager, then setup and teardown relevent test cases
- for business logic that use clients
- use (verified) fake clients that follow a protocol
- enforce contract verification tests between the fake objects and the real objects
- ensure the behaviour is valid (ie the inputs and outputs match expectations)
- use (verified) fake clients that follow a protocol
- for all tests
-
https://martinfowler.com/articles/mocksArentStubs.html
- dummy objects (passed around but never used)
- fake objects (working implementation with some shortcut ie in-memory database)
- stubs (provide canned answers to specific calls)
- spies (stubs that record usage information)
- mocks (objects pre-programmed with expectations)
-
https://pythonspeed.com/articles/verified-fakes/
- testing external services; slow, or fake and uncertain
- use test doubles
- this ensures you match the signaure but not the behaviour
- example
MessageService → TwitterClientTests → MessageService → TwitterClientTests → MessageService → FakeTwitterClient
- use verified fakes (when the API is slow or you want to test frequently)
- two tests, once against the real client and once against the fake client (contract verification tests)
- example
TwitterContractTests → TwitterClientTwitterContractTests → FakeTwitterClient
-
https://www.b-list.org/weblog/2023/dec/08/mock-python-httpx/
- make testing easier
- dependency injection helps with this
- make testing easier
https://hynek.me/articles/what-to-mock-in-5-mins/
-
don't mock what you don't own
-
avoid the need to use multiple layers of mocks and patches, or worry about the orderings and references, brittle when module paths are refactored
-
ensure determinism in results using stubs
-
you may have business logic tests, but spend lots of time with boilerplate trying to mimic a client whose API may change ay any time, are you even testing what you want to test
-
https://nedbatchelder.com/blog/202202/why_your_mock_still_doesnt_work.html
- mocks are dangerous
- you can call any method or access any attribute and they will return another mock
- this can silence errors if patch ordering is incorrect or if methods and attributes don't exist
- use
spec=Trueandautospec=True(which recursively applies to any new mocks from the original mock)
- mocks are dangerous
from unittest.mock import patch
@patch("httpx.Client")
def test_httpx(mock_client):
mock_client.fake_method_that_doesnt_exist() # passes
mock_client.another_fake_thing() # passes
mock_client.xyz.abc() # passes
print("All fake methods accepted! 😱")