Test assertions#
A useful test assertion describes the complete behavior that must remain stable. Comparing a whole response catches missing fields, unexpected fields, and changed values in one place. It also produces a single diff that is easier to review than a sequence of assertions against individual fields.
Not every value is stable, though. Database identifiers, UUIDs, timestamps, and values close to a timing boundary are expected to change between test runs. inline-snapshot and dirty-equals express this distinction:
Tool |
Use it when |
What it verifies |
|---|---|---|
The complete value is deterministic and worth asserting. |
The exact value, stored next to the assertion and reviewed as code. |
|
A value is generated at runtime or can vary for a known reason. |
Its type, shape, format, range, or other relevant properties. |
These tools are not alternatives. A snapshot can describe a stable container
while dirty-equals matchers describe its dynamic values.
Tip
This guide focuses on choosing an assertion strategy and shows only a small set of features. Refer to the full inline-snapshot documentation and dirty-equals documentation for all supported snapshot operations, matchers, and configuration options.
Assert deterministic values with inline-snapshot#
Use snapshot() when every value in the expected result is deterministic.
Validation errors are a good example: the complete error response is part of
the API contract, including the number and order of errors, their locations,
messages, and types.
1import json
2from http import HTTPStatus
3
4from django.http import HttpResponse
5from inline_snapshot import snapshot
6
7from dmr.test import DMRRequestFactory
8from examples.testing.pydantic_controller import UserController
9
10
11def test_complete_validation_error(dmr_rf: DMRRequestFactory) -> None:
12 request = dmr_rf.post('/users/', data={})
13
14 response = UserController.as_view()(request)
15
16 assert isinstance(response, HttpResponse)
17 assert response.status_code == HTTPStatus.BAD_REQUEST
18 assert json.loads(response.content) == snapshot({
19 'detail': [
20 {
21 'msg': 'Field required',
22 'loc': ['parsed_body', 'email'],
23 'type': 'value_error',
24 },
25 {
26 'msg': 'Field required',
27 'loc': ['parsed_body', 'age'],
28 'type': 'value_error',
29 },
30 ],
31 })
One possible workflow is:
Write the assertion with an empty
snapshot()call.Run the focused test with
--inline-snapshot=createor--fix.Inspect the generated value and its diff. A snapshot is expected test code, not an automatically approved result.
Run the test again normally. The committed literal is then compared exactly on every run.
Inline snapshots are especially convenient for nested dictionaries and lists: the expected value stays beside the behavior it documents, and an intentional contract change produces an ordinary source diff.
Tip
inline-snapshot supports more workflows and value types than this basic
example demonstrates. See the inline-snapshot documentation when you need
to create, update, review, or make an existing snapshot more precise.
Combine exact values and dynamic matchers#
Do not freeze a value that is supposed to change. Instead, use the narrowest
dirty-equals matcher that describes the behavior the application promises.
It can be used directly alongside exact values:
1import json
2from http import HTTPStatus
3
4from dirty_equals import IsUUID
5from django.http import HttpResponse
6
7from dmr.test import DMRRequestFactory
8from examples.testing.pydantic_controller import UserController
9
10
11def test_dynamic_user_identifier(dmr_rf: DMRRequestFactory) -> None:
12 request_data = {'email': 'test@example.com', 'age': 43}
13 request = dmr_rf.post('/users/', data=request_data)
14
15 response = UserController.as_view()(request)
16
17 assert isinstance(response, HttpResponse)
18 assert response.status_code == HTTPStatus.CREATED
19 assert json.loads(response.content) == {
20 'uid': IsUUID(),
21 **request_data,
22 }
This assertion checks the complete response. The UUID is not ignored:
IsUUID() verifies its format, while the deterministic email and age remain
exact.
When the complete response is also worth preserving as a snapshot, put the matcher at the dynamic leaf:
1import json
2from http import HTTPStatus
3
4from dirty_equals import IsUUID
5from django.http import HttpResponse
6from faker import Faker
7from inline_snapshot import snapshot
8
9from dmr.test import DMRRequestFactory
10from examples.testing.pydantic_controller import UserController
11
12
13def test_complete_user_response(
14 dmr_rf: DMRRequestFactory,
15 faker: Faker,
16) -> None:
17 email = faker.email()
18 request = dmr_rf.post(
19 '/users/',
20 data={'email': email, 'age': 43},
21 )
22
23 response = UserController.as_view()(request)
24
25 assert isinstance(response, HttpResponse)
26 assert response.status_code == HTTPStatus.CREATED
27 assert json.loads(response.content) == snapshot({
28 'uid': IsUUID(),
29 'email': email,
30 'age': 43,
31 })
This composition gives the test both properties we want: changing the response shape or a deterministic field produces a snapshot diff, while a newly generated UUID remains valid without making the test flaky.
Tip
dirty-equals includes specialized matchers for strings, numbers, dates,
mappings, iterables, instances, and other common values. Browse the complete
dirty-equals documentation before using a broad matcher; a more expressive
matcher might already describe the property you need. The
dirty-equals string matcher documentation shows, for example, how to
constrain length, case, and regular expressions.