Testing
By the end: You can write tests that catch real regressions and survive refactoring.
- pytest basics
- Testing the same logic against many inputs
- Fixtures for shared setup
- Fakes at the boundary
- What you can and cannot test in an AI system
New to python? A programming language is the set of written instructions a computer will follow, and Python is the one this field settled on. It reads close to English: a line that filters a list of records looks roughly like a sentence describing that filter. Every model provider ships a Python library, and nearly every machine learning tool is written in it. Why it matters
A test is a small piece of code that runs your code and checks the answer. The reason to write them is not diligence: it is that they let you change something six months later and find out in seconds whether you broke anything. This section covers pytest, running the same logic against many inputs, sharing setup between tests, and standing in fakes for slow or paid dependencies — which matters in AI work, where a real model call costs money and answers differently each time.
Your first test#
A test is a function whose name starts with test_ containing a plain assert. pytest finds them and runs them. There is no base class and no special assertion methods.
# add.py
def add(a, b):
return a + b
# test_add.py
from add import add
def test_adds_two_numbers():
assert add(2, 3) == 5
def test_handles_negatives():
assert add(-1, 1) == 0pytest # run everything
pytest test_add.py # one file
pytest -k "negative" # only tests matching a name
pytest -v # show each test nameMany inputs, one test#
parametrize runs the same test once per set of inputs, and reports each one separately when it fails. This replaces copying a test five times with different values.
import pytest
@pytest.mark.parametrize(
"raw, expected",
[
("42", 42),
(" 42 ", 42),
("0", 0),
],
)
def test_parses_int(raw, expected):
assert parse_int(raw) == expectedTesting that something raises#
def test_rejects_bad_temperature():
with pytest.raises(ValueError):
set_temperature(5)
def test_error_message_is_useful():
with pytest.raises(ValueError, match="must be 0 to 2"):
set_temperature(5)Fixtures#
A fixture is setup shared between tests. Declare it once, then take it as an argument in any test that needs it.
@pytest.fixture
def sample_records():
return [
{"id": "1", "score": 0.9},
{"id": "2", "score": 0.4},
]
def test_filters_by_score(sample_records):
result = above(sample_records, 0.5)
assert len(result) == 1Fakes, not mocks everywhere#
Replace the thing at the edge of your system, such as the model client, with a simple object that returns fixed answers. Do not replace your own functions with mocks. A test that only checks a mock was called proves nothing and keeps passing after you break the behaviour.
class FakeModel:
def __init__(self, replies):
self.replies = list(replies)
self.calls = 0
def call(self, prompt):
self.calls += 1
return self.replies.pop(0)
def test_agent_stops_at_turn_limit():
model = FakeModel(["think", "think", "think"])
agent = Agent(model=model, max_turns=2)
agent.run("anything")
assert model.calls == 2What you can test in an AI system#
More than people expect. The model's output is not predictable, but everything around it is, and that is where most bugs live.
- Testable normally: parsing responses, retry logic, budget and turn limits, tool dispatch, building the message list, cost calculation.
- Not unit testable: whether the model's answer is good. That needs evals, which are a different tool for a different job.
Keep the two apart
Separating deterministic logic from model-dependent behaviour is what makes an AI system debuggable. If the parsing and the judgement are tangled in one function, you cannot test either of them.
Where people go wrong
Writing tests that assert on exact model output. Model output varies between runs even at temperature zero, so the test flakes and eventually gets deleted. Assert on structure and properties instead.
If this comes up in an interview
What is the difference between a unit test and an integration test?
A unit test checks one piece in isolation with fast, predictable inputs. An integration test checks that several pieces work together, usually including something real such as a database. You want many of the first and a few of the second.
Why is mocking heavily a problem?
Tests full of mocks assert that your code called your code, which stays true even after you break the behaviour. They also have to be rewritten on every refactor. Fake at the system boundary and let the real logic run.
Is 100% test coverage a good goal?
No. Coverage tells you which lines ran, not whether anything was verified. A suite at 90% that asserts almost nothing is worse than an honest 50%, because it creates confidence that has not been earned.
How do you test code that calls an LLM?
Separate the deterministic parts and test those normally with a fake client returning fixed responses. The quality of the model's output is not a unit test; it needs an eval suite measured over a fixed task set.
Build this: Write tests for a retry function: it succeeds first time, it succeeds after two failures, and it gives up after the limit. Assert on the number of attempts made, not on a mock.
This is one section of Python, 33 hours across 9 sections. Read it in full to get the whole topic in order.
Stop 1 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.