Pytest snapshot testing utility that enables developers to write tests asserting immutability of computed results.
Overall
score
80%
Value transformation system for handling dynamic data in snapshots. Matchers allow replacing or transforming specific values during snapshot comparison, essential for testing data with timestamps, IDs, or other non-deterministic values.
Transform values based on their path location and data type, replacing dynamic values with stable placeholders.
def path_type(mapping: Optional[Dict[str, Tuple[PropertyValueType, ...]]] = None, *, types: Tuple[PropertyValueType, ...] = (), strict: bool = True, regex: bool = False, replacer: Replacer = ...) -> PropertyMatcher:
"""
Create matcher that replaces values based on path and type.
Parameters:
- mapping: Dictionary mapping path patterns to (type, replacement) tuples
- strict: Whether to raise error on type mismatches
Returns:
PropertyMatcher: Function that transforms matching values
Raises:
PathTypeError: When path exists but type doesn't match (strict=True)
"""Usage examples:
def test_dynamic_timestamp(snapshot):
from syrupy.matchers import path_type
result = {
"user_id": 12345, # Dynamic ID
"timestamp": "2023-12-01T10:30:45Z", # Dynamic timestamp
"message": "Hello world", # Static content
"metadata": {
"request_id": "req_abc123", # Dynamic nested ID
"version": "1.0" # Static nested content
}
}
# Replace dynamic values with stable placeholders
assert result == snapshot(matcher=path_type({
"user_id": (int, "<user_id>"),
"timestamp": (str, "<timestamp>"),
"metadata.request_id": (str, "<request_id>")
}))
def test_list_with_dynamic_ids(snapshot):
from syrupy.matchers import path_type
users = [
{"id": 101, "name": "Alice", "created": "2023-12-01"},
{"id": 102, "name": "Bob", "created": "2023-12-02"}
]
# Replace all ID and created fields regardless of list position
assert users == snapshot(matcher=path_type({
"*.id": (int, "<id>"),
"*.created": (str, "<created>")
}))
def test_strict_type_checking(snapshot):
from syrupy.matchers import path_type, PathTypeError
data = {"age": "25"} # String instead of expected int
try:
# This will raise PathTypeError because age is str, not int
assert data == snapshot(matcher=path_type({
"age": (int, "<age>")
}, strict=True))
except PathTypeError:
# Handle type mismatch
pass
# With strict=False, mismatched types are ignored
assert data == snapshot(matcher=path_type({
"age": (int, "<age>")
}, strict=False))Transform values based on regex patterns matched against their string representation.
def path_value(mapping: Dict[str, Any]) -> PropertyMatcher:
"""
Create matcher that replaces values based on path regex patterns.
Parameters:
- mapping: Dictionary mapping path regex patterns to replacement values
Returns:
PropertyMatcher: Function that transforms matching string representations
"""Usage examples:
def test_regex_patterns(snapshot):
from syrupy.matchers import path_value
data = {
"email": "user@example.com",
"phone": "+1-555-123-4567",
"website": "https://example.com/path",
"static_field": "keep this"
}
# Use regex patterns to match and replace values
assert data == snapshot(matcher=path_value({
r"email": "<email>", # Exact path match
r"phone": "<phone>", # Replace phone number
r".*url.*": "<url>" # Pattern matching paths containing 'url'
}))
def test_complex_patterns(snapshot):
from syrupy.matchers import path_value
response = {
"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"session_id": "sess_1234567890abcdef",
"api_key": "ak_live_1234567890abcdef1234567890abcdef",
"user_data": {"name": "Alice"}
}
# Replace tokens and IDs with patterns
assert response == snapshot(matcher=path_value({
r".*token.*": "<token>", # Any path containing 'token'
r".*session.*": "<session>", # Any path containing 'session'
r".*key.*": "<api_key>" # Any path containing 'key'
}))Combine multiple matchers for complex transformation scenarios.
def compose_matchers(*matchers: PropertyMatcher) -> PropertyMatcher:
"""
Compose multiple matchers into a single matcher function.
Parameters:
- *matchers: Variable number of matcher functions to compose
Returns:
PropertyMatcher: Combined matcher applying all transformations in sequence
"""Usage examples:
def test_composed_matchers(snapshot):
from syrupy.matchers import path_type, path_value, compose_matchers
data = {
"user_id": 12345, # Handle by type
"email": "user@domain.com", # Handle by regex pattern
"timestamp": "2023-12-01T10:30:45.123Z", # Handle by type
"session_token": "tok_abc123def456", # Handle by regex pattern
"name": "Alice" # Leave unchanged
}
# Combine type-based and regex-based matching
combined_matcher = compose_matchers(
path_type({
"user_id": (int, "<user_id>"),
"timestamp": (str, "<timestamp>")
}),
path_value({
r".*email.*": "<email>",
r".*token.*": "<token>"
})
)
assert data == snapshot(matcher=combined_matcher)Create custom matcher functions for specialized transformation needs.
PropertyMatcher = Callable[[SerializableData, PropertyPath], SerializableData]
# PropertyPath structure
PropertyPathEntry = Tuple[PropertyName, PropertyValueType]
PropertyPath = Tuple[PropertyPathEntry, ...]Usage examples:
def test_custom_matcher(snapshot):
def uuid_matcher(data, path):
"""Custom matcher for UUID fields"""
import re
# Get the property name from the path
if path and len(path) > 0:
prop_name = path[-1][0] # Last entry's property name
# Check if it looks like a UUID field and value
if "id" in str(prop_name).lower() and isinstance(data, str):
uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
if re.match(uuid_pattern, data, re.IGNORECASE):
return "<uuid>"
return data # Return unchanged if not a UUID
result = {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice",
"session_id": "123e4567-e89b-12d3-a456-426614174000"
}
assert result == snapshot(matcher=uuid_matcher)
def test_date_normalizer(snapshot):
from datetime import datetime
def date_normalizer(data, path):
"""Normalize various date formats to ISO format"""
if isinstance(data, str):
try:
# Try parsing common date formats
for fmt in ["%Y-%m-%d", "%m/%d/%Y", "%d-%m-%Y"]:
try:
parsed = datetime.strptime(data, fmt)
return parsed.strftime("%Y-%m-%d")
except ValueError:
continue
except:
pass
return data
data = {
"start_date": "12/25/2023", # MM/DD/YYYY format
"end_date": "25-12-2023", # DD-MM-YYYY format
"created": "2023-12-25" # Already ISO format
}
# All dates will be normalized to YYYY-MM-DD format
assert data == snapshot(matcher=date_normalizer)Matchers provide specific exceptions for error handling:
class PathTypeError(Exception):
"""Raised when path type matching fails"""
class StrictPathTypeError(PathTypeError):
"""Raised when strict path type matching fails"""Usage examples:
def test_matcher_error_handling(snapshot):
from syrupy.matchers import path_type, PathTypeError
data = {"count": "not_a_number"}
try:
# This will raise PathTypeError in strict mode
assert data == snapshot(matcher=path_type({
"count": (int, "<count>")
}, strict=True))
except PathTypeError as e:
# Handle the type mismatch error
print(f"Type mismatch: {e}")
# Use non-strict mode as fallback
assert data == snapshot(matcher=path_type({
"count": (int, "<count>")
}, strict=False))Install with Tessl CLI
npx tessl i tessl/pypi-syrupyevals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10