Snapshot testing treats your complex, generated output as a "snapshot" that you commit to version control.

Here’s a pytest snapshot test in action. Imagine you have a function that generates a JSON string representing a complex data structure:

# your_module.py
import json

def generate_complex_data():
    return {
        "user": {
            "id": 123,
            "name": "Alice",
            "roles": ["admin", "editor"],
            "settings": {
                "theme": "dark",
                "notifications": True
            }
        },
        "timestamp": "2023-10-27T10:00:00Z",
        "data": [1, 2, 3, 4, 5]
    }

You want to ensure this output remains consistent. With pytest-snapshot, you’d write a test like this:

# test_your_module.py
from your_module import generate_complex_data
from syrupy.assertion import SnapshotAssertion

def test_complex_data_snapshot(snapshot: SnapshotAssertion):
    output = generate_complex_data()
    assert output == snapshot

When you run this test for the first time (pytest), pytest-snapshot will see that snapshot doesn’t exist for this test. It will generate a file (e.g., test_your_module.ambr) in a __snapshots__ directory next to your test file. This file will contain the serialized output of generate_complex_data():

# __snapshots__/test_your_module.ambr
# -*- coding: utf-8 -*-
# snapshottest: v1
---
{
  "data": [
    1,
    2,
    3,
    4,
    5
  ],
  "timestamp": "2023-10-27T10:00:00Z",
  "user": {
    "id": 123,
    "name": "Alice",
    "roles": [
      "admin",
      "editor"
    ],
    "settings": {
      "notifications": true,
      "theme": "dark"
    }
  }
}

You then commit this .ambr file along with your code. On subsequent runs, pytest-snapshot will load the committed snapshot and compare it against the actual output of generate_complex_data(). If they differ, the test fails, and you get a diff.

The problem snapshot testing solves is the brittle nature of manually asserting complex, structured outputs like JSON, HTML, or large dictionaries. Writing individual assertions for every key, nested value, and array element is tedious and prone to errors. More importantly, it’s hard to see the forest for the trees. A snapshot gives you the entire structure at once.

Internally, pytest-snapshot uses syrupy, a powerful snapshot testing library. When you use snapshot == actual_output, syrupy serializes actual_output (handling various types like dictionaries, lists, strings, etc.) and compares it to the stored snapshot. If it’s the first run or if you explicitly tell it to update (pytest --snapshot-update), it writes the new output to the .ambr file.

The exact levers you control are primarily through syrupy’s configuration and the SnapshotAssertion object. You can customize serializers for specific data types, change the snapshot file extension, and specify where snapshots are stored. For instance, if your generate_complex_data function produced an object with custom methods, you might need to register a custom serializer to ensure its state is captured correctly.

The SnapshotAssertion object itself is a proxy. When you write output == snapshot, the snapshot object intercepts the comparison. It loads the previously saved snapshot data and passes both the loaded data and your output to syrupy’s comparison engine. If the comparison fails, it generates a diff report.

A key aspect is how syrupy handles different data types. By default, it has built-in serializers for common Python types and can often infer how to serialize custom objects if they have a __dict__ or __slots__ attribute. However, for truly complex or custom objects, you might need to explicitly tell syrupy how to serialize them by creating a custom serializer and registering it. This is often done in a conftest.py file, enabling consistent snapshotting across your project. For example, if you had a User class with a specific string representation you wanted to capture, you’d write a serializer that converts User instances into that string representation before it’s stored.

The next concept you’ll likely encounter is managing snapshot evolution, particularly when your code intentionally changes output and you need to update the snapshots gracefully.

Want structured learning?

Take the full Pytest course →