How Snapshots Work¶
The snapshot fixture is the core of pytest-ditto. It stores a test's output
the first time the test runs, and gives it back on every later run so the test
can check that the output hasn't changed.
The snapshot fixture¶
def test_fn(snapshot) -> None:
result = compute_something()
assert result == snapshot(result, key="something")
The snapshot callable takes two arguments:
data: the value to snapshot. It must be a value the test's recorder accepts; the default, strict JSON, rejects tuples, sets, dates and other values outside its data model.key: a string naming this snapshot, unique within the test. A key that isn't astrraisesTypeError.
snapshot() returns the value for the test to compare against. That value is
always what the recorder reads back, never data itself, so a test sees on its
first run exactly what every later run will see.
Record, update and verify¶
What snapshot() does depends on the run's mode and on whether the snapshot is
already stored:
graph TD
A["snapshot(data, key)"] --> B{Mode}
B -->|"record (default)"| C{Stored?}
C -->|Yes| D[Return the stored value]
C -->|No| E[Store data, return it as read back]
B -->|"update (--ditto-update)"| F[Store data, return it as read back]
B -->|"verify (--ditto-verify)"| G{Stored?}
G -->|Yes| H[Return the stored value]
G -->|No| I[Store nothing, return data as read back]
| Mode | Snapshot stored | Snapshot missing |
|---|---|---|
record: a plain pytest run |
Returns the stored value. | Stores data, then returns it as read back. |
update: ditto update, --ditto-update |
Overwrites it with data, then returns data as read back. |
Stores data, then returns it as read back. |
verify: ditto verify, --ditto-verify |
Returns the stored value. | Stores nothing and returns data as read back; the run then fails, reporting the snapshot. |
"As read back" means data is serialised and deserialised before it is
returned, and nothing is stored if the recorder can't read its own output. So a
value the recorder doesn't store exactly fails on the run that records it,
rather than on the next one. The YAML recorder, for example, reads a tuple back
as a list:
import ditto
@ditto.yaml
def test_pair(snapshot):
assert (1, 2) == snapshot((1, 2), key="pair") # fails: (1, 2) != [1, 2]
Snapshot a list instead, or choose a recorder that keeps the distinction.
A missing snapshot passes in record mode
In record mode a missing snapshot is stored and the test passes, so a
deleted or renamed snapshot is silently re-recorded from the code's current
output. In CI, run ditto verify: it never writes, and
it fails on any missing snapshot.
Several snapshots in one test¶
Give each snapshot its own key:
def test_pipeline(snapshot):
raw = fetch_data()
processed = transform(raw)
assert raw == snapshot(raw, key="raw_input")
assert processed == snapshot(processed, key="transformed")
Using the same key twice in one test raises DuplicateSnapshotKeyError:
def test_bad(snapshot):
snapshot(1, key="x")
snapshot(2, key="x") # raises DuplicateSnapshotKeyError
Choosing a recorder and a target¶
A test's snapshots use the strict JSON recorder and are stored in a .ditto/
directory next to the test file, unless a record mark says otherwise. The
mark names a recorder, and can also name a
target, the place snapshots are stored:
import ditto
@ditto.yaml
def test_config(snapshot): ...
@ditto.record("json", target="s3://my-bucket/snapshots/")
def test_remote(snapshot): ...
@ditto.yaml is shorthand for @ditto.record("yaml"), and takes the same
keyword arguments: @ditto.yaml(target="s3://my-bucket/snapshots/"). A target
always comes with a recorder: @ditto.record(target=...) on its own is an
error, so use @ditto.json(target=...) for the default format.
A mark applies to a test function, or to every test in a class or module when
set there, for example with pytestmark = ditto.yaml. A test can have only
one record mark: a function mark on top of a class or module mark raises
AdditionalMarkError rather than overriding it.
Where snapshots are stored¶
Each snapshot is stored under a name built from its test module, test, key and
recorder, such as
tests/.ditto/tests.test_api.test_response@body~234c7156f10c6aa4.json. See
Snapshot Names.
A snapshot test must live under pytest's rootdir, because its name starts with the test file's path relative to the rootdir. A test outside it fails with an error that says so.
Keeping snapshots current¶
When your code's output changes on purpose, re-record its snapshots with
ditto update. When you remove or rename tests, rebuild the lock with
ditto lock and delete the snapshots nothing uses with ditto prune. See
Maintaining Snapshots.