Pytest snapshot testing utility that enables developers to write tests asserting immutability of computed results.
npx @tessl/cli install tessl/pypi-syrupy@4.9.0A zero-dependency pytest snapshot testing library that enables developers to write tests asserting immutability of computed results. Syrupy provides an extensible and idiomatic approach to snapshot testing with the syntax assert x == snapshot, offering comprehensive filtering, matching, and custom serialization capabilities.
pip install syrupyimport syrupyStandard usage in tests:
def test_example(snapshot):
assert result == snapshotExtension imports:
from syrupy.extensions.amber import AmberSnapshotExtension
from syrupy.extensions.json import JSONSnapshotExtension
from syrupy.extensions.image import PNGImageSnapshotExtension, SVGImageSnapshotExtension
from syrupy.extensions.single_file import SingleFileSnapshotExtension, SingleFileAmberSnapshotExtension, WriteModeMatcher and filter imports:
from syrupy.matchers import path_type, path_value, compose_matchers
from syrupy.filters import props, paths, paths_includeException imports:
from syrupy.exceptions import SnapshotDoesNotExist, FailedToLoadModuleMember, TaintedSnapshotError
from syrupy.matchers import PathTypeError, StrictPathTypeErrordef test_basic_snapshot(snapshot):
"""Basic snapshot testing - creates .ambr file in __snapshots__ directory"""
actual = {"name": "Alice", "age": 30, "scores": [85, 92, 78]}
assert actual == snapshot
def test_with_index(snapshot):
"""Multiple snapshots in one test"""
assert "first result" == snapshot
assert "second result" == snapshot
def test_custom_extension(snapshot):
"""Using JSON extension for dictionary data"""
from syrupy.extensions.json import JSONSnapshotExtension
data = {"users": [{"id": 1, "name": "Alice"}]}
assert data == snapshot.use_extension(JSONSnapshotExtension)
def test_with_matchers(snapshot):
"""Using matchers to handle dynamic data"""
from syrupy.matchers import path_type
result = {
"timestamp": "2023-12-01T10:30:00Z", # Dynamic value
"user_id": 12345, # Dynamic value
"message": "Hello world" # Static value
}
# Replace dynamic values with placeholders
assert result == snapshot(matcher=path_type({
"timestamp": (str, "<timestamp>"),
"user_id": (int, "<user_id>")
}))
def test_with_filters(snapshot):
"""Using filters to include/exclude properties"""
from syrupy.filters import props
data = {
"public_field": "visible",
"private_field": "hidden",
"internal_field": "secret"
}
# Only include public fields
assert data == snapshot(include=props("public_field"))Syrupy uses an extensible architecture built around several key components:
The pytest plugin automatically registers hooks and provides the snapshot fixture, making snapshot testing seamlessly integrate with existing test suites.
Primary snapshot assertion functionality with fluent interface for configuration, including basic assertions, indexed snapshots, and method chaining for custom behavior.
class SnapshotAssertion:
def __call__(self, *, matcher=None, include=None, exclude=None, extension_class=None): ...
def __eq__(self, other): ...
def use_extension(self, extension_class): ...
def with_defaults(self, **kwargs): ...Pluggable serialization and storage system supporting multiple output formats including Amber (default multi-snapshot), JSON, PNG, SVG, and raw file formats with extensible base classes.
class AbstractSyrupyExtension: ...
class AmberSnapshotExtension(AbstractSyrupyExtension): ...
class JSONSnapshotExtension(AbstractSyrupyExtension): ...
class PNGImageSnapshotExtension(AbstractSyrupyExtension): ...
class SVGImageSnapshotExtension(AbstractSyrupyExtension): ...Value transformation system for handling dynamic data in snapshots, providing path-based matching with type replacement and regex-based value substitution.
def path_type(mapping: Dict[str, Tuple[type, Any]], *, strict: bool = True): ...
def path_value(mapping: Dict[str, Any]): ...
def compose_matchers(*matchers): ...Property inclusion and exclusion system for controlling serialization scope, supporting path-based and property-based filtering with nested path handling.
def props(*included: str): ...
def paths(*included: str): ...
def paths_include(*nested_paths: str): ...Command-line options and pytest integration providing snapshot update modes, reporting configuration, and extension selection.
def pytest_addoption(parser): ...
@pytest.fixture
def snapshot(request): ...from typing import Union, Any, Callable, Tuple, Hashable, Type, List, Optional
import re
# Core types
SnapshotIndex = Union[int, str]
SerializableData = Any
SerializedData = Union[str, bytes]
# Property system types
PropertyName = Hashable
PropertyValueType = Type[SerializableData]
PropertyPathEntry = Tuple[PropertyName, PropertyValueType]
PropertyPath = Tuple[PropertyPathEntry, ...]
PropertyMatcher = Callable[[SerializableData, PropertyPath], Optional[SerializableData]]
PropertyFilter = Callable[[PropertyName, PropertyPath], bool]
# Matcher helper types
try:
MatchResult = Optional[re.Match[str]]
except TypeError:
MatchResult = Optional[re.Match]
Replacer = Callable[[SerializableData, MatchResult], SerializableData]
# Assertion result
@dataclass
class AssertionResult:
snapshot_location: str
snapshot_name: str
asserted_data: Optional[SerializedData]
recalled_data: Optional[SerializedData]
created: bool
updated: bool
success: bool
exception: Optional[Exception]
test_location: "PyTestLocation"
@property
def final_data(self) -> Optional[SerializedData]: ...
# Diff mode
class DiffMode(Enum):
DETAILED = "detailed"
DISABLED = "disabled"class SnapshotDoesNotExist(Exception):
"""Raised when a snapshot file or entry doesn't exist"""
class FailedToLoadModuleMember(Exception):
"""Raised when unable to load a module member"""
class TaintedSnapshotError(Exception):
"""Raised when snapshot is corrupted and needs regeneration"""
class PathTypeError(TypeError):
"""Raised when path type matching encounters an error"""
class StrictPathTypeError(PathTypeError):
"""Raised when strict path type matching fails due to type mismatch"""