Skip to content

Snapshot assertions

Compare a value against a stored snapshot, recording it on first run.

Snapshot mixin.

Take a snapshot of a python data structure, store it on disk in JSON format, and automatically compare the latest data to the stored data on every test run.

Functional testing (which snapshot testing falls under) is very much blackbox testing. When something goes wrong, it's hard to pinpoint the issue, because functional tests typically provide minimal isolation as compared to unit tests. On the plus side, snapshots typically do provide enormous leverage as a few well-placed snapshot tests can strongly verify that an application is working. Similar coverage would otherwise require dozens if not hundreds of unit tests.

On-disk Format

Snapshots are stored in a readable JSON format. For example:

assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot()

Would be stored as:

{
    "a": 1,
    "b": 2,
    "c": 3
}

The JSON formatting support most python data structures (dict, list, object, etc), but not custom binary data.

Updating

It's easy to update your snapshots...just delete them all and re-run the test suite to regenerate all snapshots.

snapshot

snapshot(
    id: str | None = None, path: str = "__snapshots"
) -> Self

Asserts that val is identical to the on-disk snapshot stored previously.

On the first run of a test before the snapshot file has been saved, a snapshot is created, stored to disk, and the test always passes. But on all subsequent runs, val is compared to the on-disk snapshot, and the test fails if they don't match.

Snapshot artifacts are stored in the __snapshots directory by default, and should be committed to source control alongside any code changes.

Snapshots are identified by test filename plus line number by default.

Parameters:

Name Type Description Default
id str | None

a custom snapshot identifier (defaults to test filename plus line number)

None
path str

the directory where snapshots are stored (defaults to __snapshots)

'__snapshots'

Examples:

Usage:

assert_that(None).snapshot()
assert_that(True).snapshot()
assert_that(1).snapshot()
assert_that(123.4).snapshot()
assert_that('foo').snapshot()
assert_that([1, 2, 3]).snapshot()
assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot()
assert_that({'a', 'b', 'c'}).snapshot()
assert_that(1 + 2j).snapshot()
assert_that(someobj).snapshot()

By default, snapshots are identified by test filename plus line number. Alternately, you can specify a custom identifier using the id arg:

assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(id='foo-id')

By default, snapshots are stored in the __snapshots directory. Alternately, you can specify a custom path using the path arg:

assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(path='my-custom-folder')

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if val does not equal to on-disk snapshot

Source code in assertpy2/snapshot.py
def snapshot(self, id: str | None = None, path: str = "__snapshots") -> Self:  # noqa: A002  # `id` is the public snapshot-identifier parameter
    """Asserts that val is identical to the on-disk snapshot stored previously.

    On the first run of a test before the snapshot file has been saved, a snapshot is created,
    stored to disk, and the test *always* passes.  But on all subsequent runs, val is compared
    to the on-disk snapshot, and the test fails if they don't match.

    Snapshot artifacts are stored in the ``__snapshots`` directory by default, and should be
    committed to source control alongside any code changes.

    Snapshots are identified by test filename plus line number by default.

    Args:
        id: a custom snapshot identifier (defaults to test filename plus line number)
        path: the directory where snapshots are stored (defaults to ``__snapshots``)

    Examples:
        Usage:

            assert_that(None).snapshot()
            assert_that(True).snapshot()
            assert_that(1).snapshot()
            assert_that(123.4).snapshot()
            assert_that('foo').snapshot()
            assert_that([1, 2, 3]).snapshot()
            assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot()
            assert_that({'a', 'b', 'c'}).snapshot()
            assert_that(1 + 2j).snapshot()
            assert_that(someobj).snapshot()

        By default, snapshots are identified by test filename plus line number.
        Alternately, you can specify a custom identifier using the ``id`` arg:

            assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(id='foo-id')


        By default, snapshots are stored in the ``__snapshots`` directory.
        Alternately, you can specify a custom path using the ``path`` arg:

            assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(path='my-custom-folder')

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if val does **not** equal to on-disk snapshot
    """
    lineno = ""
    if id:
        # custom id
        snapname = _name(path, id)
    else:
        # make id from filename and line number
        frame = inspect.currentframe()
        caller = frame.f_back if frame is not None else None
        if caller is None:  # pragma: no cover - frame introspection always available in CPython
            raise RuntimeError("cannot determine caller frame")
        file_path = os.path.basename(caller.f_code.co_filename)
        file_name = os.path.splitext(file_path)[0]
        lineno = str(caller.f_lineno)
        snapname = _name(path, file_name)

    os.makedirs(path, exist_ok=True)

    # Serialize read-modify-write so parallel workers (pytest-xdist) sharing a snap file don't lose
    # each other's entries.  The comparison runs after the lock is released.
    snapshot_value = _UNSET
    with _file_lock(snapname):
        if os.path.isfile(snapname):
            snap = _load(snapname)
            if id:
                # custom id, so test against the whole file
                snapshot_value = snap
            elif lineno in snap:
                # found sub-snap, so test
                snapshot_value = snap[lineno]
            else:
                # lineno not in snap, so create sub-snap and pass
                snap[lineno] = self.val
                _save(snapname, snap)
        else:
            # no snap, so create and pass
            _save(snapname, self.val if id else {lineno: self.val})

    if snapshot_value is not _UNSET:
        return self.is_equal_to(snapshot_value)
    return self