Skip to main content

PyTest Snapshot Test Utility

Project description

syrupy

Logo

All Contributors Maturity badge - level 4 Stage Discord

Pytest>=5.1.0,<7.0.0 Pypi Wheel PyPI - Python Version PyPI - Downloads PyPI - License

Build Status codecov

Overview

Syrupy is a pytest snapshot plugin. It enables developers to write tests which assert immutability of computed results.

Motivation

The most popular snapshot test plugin compatible with pytest has some core limitations which this package attempts to address by upholding some key values:

  • Extensible: If a particular data type is not supported, users should be able to easily and quickly add support.
  • Idiomatic: Snapshot testing should fit naturally among other test cases in pytest, e.g. assert x == snapshot vs. snapshot.assert_match(x).
  • Soundness: Snapshot tests should uncover even the most minute issues. Unlike other snapshot libraries, Syrupy will fail a test suite if a snapshot does not exist, not just on snapshot differences.

Installation

python -m pip install syrupy

Migration from snapshottest

You cannot use syrupy alongside snapshottest due to argument conflicts. To ease migration, we've made syrupy aware of snapshottest call syntax. Simply uninstall snapshottest and remove old snapshots:

pip uninstall snapshottest -y;
find . -type d ! -path '*/\.*' -name 'snapshots' | xargs rm -r

Usage

Basic Usage

In a pytest test file test_file.py:

def test_foo(snapshot):
    actual = "Some computed value!"
    assert actual == snapshot

when you run pytest, the above test should fail due to a missing snapshot. Re-run pytest with the update snapshots flag like so:

pytest --snapshot-update

A snapshot file should be generated under a __snapshots__ directory in the same directory as test_file.py. The __snapshots__ directory and all its children should be committed along with your test code.

Usage Demo

Custom Objects

The default serializer supports all python built-in types and provides a sensible default for custom objects.

Representation

If you need to customise your object snapshot, it is as easy as overriding the default __repr__ implementation.

def __repr__(self) -> str:
    return "MyCustomClass(...)"

Attributes

If you want to limit what properties are serialized at a class type level you could either:

A. Provide a filter function to the snapshot exclude configuration option.

def limit_foo_attrs(prop, path):
  allowed_foo_attrs = {"only", "serialize", "these", "attrs"}
  return isinstance(path[-1][1], Foo) and prop in allowed_foo_attrs

def test_bar(snapshot):
    actual = new Foo(...)
    assert actual == snapshot(exclude=limit_foo_attrs)

B. Or override the __dir__ implementation to control the attribute list.

class Foo:
  def __dir__(self):
    return ["only", "serialize", "these", "attrs"]

def test_bar(snapshot):
    actual = new Foo(...)
    assert actual == snapshot

Both options will generate equivalent snapshots but the latter is only viable when you have control over the class implementation and do not need to share the exclusion logic with other objects.

CLI Options

These are the cli options exposed to pytest by the plugin.

Option Description Default
--snapshot-update Snapshots will be updated to match assertions and unused snapshots will be deleted. False
--snapshot-details Includes details of unused snapshots (test name and snapshot location) in the final report. False
--snapshot-warn-unused Prints a warning on unused snapshots rather than fail the test suite. False
--snapshot-default-extension Use to change the default snapshot extension class. AmberSnapshotExtension
--snapshot-no-colors Disable test results output highlighting. Equivalent to setting the environment variables ANSI_COLORS_DISABLED or NO_COLOR Disabled by default if not in terminal.

Assertion Options

These are the options available on the snapshot assertion fixture. Use of these options are one shot and do not persist across assertions. For more persistent options see advanced usage.

matcher

This allows you to match on a property path and value to control how specific object shapes are serialized.

The matcher is a function that takes two keyword arguments. It should return the replacement value to be serialized or the original unmutated value.

Argument Description
data Current serializable value being matched on
path Ordered path traversed to the current value e.g. (("a", dict), ("b", dict)) from { "a": { "b": { "c": 1 } } }}

NOTE: Do not mutate the value received as it could cause unintended side effects.

Built-In Matchers

Syrupy comes with built-in helpers that can be used to make easy work of using property matchers.

path_type(mapping=None, *, types=(), strict=True, regex=False)

Easy way to build a matcher that uses the path and value type to replace serialized data. When strict, this will raise a ValueError if the types specified are not matched.

Argument Description
mapping Dict of path string to tuples of class types, including primitives e.g. (MyClass, UUID, datetime, int, str)
types Tuple of class types used if none of the path strings from the mapping are matched
strict If a path is matched but the value at the path does not match one of the class types in the tuple then a PathTypeError is raised
regex If true, the mapping key is treated as a regular expression when matching paths
from syrupy.matchers import path_type

def test_bar(snapshot):
    actual = {
      "date_created": datetime.now(),
      "value": "Some computed value!!",
    }
    assert actual == snapshot(matcher=path_type({
      "date_created": (datetime,),
      "nested.path.id": (int,),
    }))
# name: test_bar
  <class 'dict'> {
    'date_created': <class 'datetime'>,
    'value': 'Some computed value!!',
  }
---

exclude

This allows you to filter out object properties from the serialized snapshot.

The exclude parameter takes a filter function that accepts two keyword arguments. It should return true or false if the property should be excluded or included respectively.

Argument Description
prop Current property on the object, could be any hashable value that can be used to retrieve a value e.g. 1, "prop_str", SomeHashableObject
path Ordered path traversed to the current value e.g. (("a", dict), ("b", dict)) from { "a": { "b": { "c": 1 } } }}
Built-In Filters

Syrupy comes with built-in helpers that can be used to make easy work of using the filter options.

props(prop_name, *prop_name)

Easy way to build a filter that excludes based on string based property names.

Takes an argument list of property names, with support for indexed iterables.

from syrupy.filters import props

def test_bar(snapshot):
    actual = {
      "id": uuid.uuid4(),
      "list": [1,2,3],
    }
    assert actual == snapshot(exclude=props("id", "1"))
# name: test_bar
  <class 'dict'> {
    'list': <class 'list'> [
      1,
      3,
    ],
  }
---
paths(path_string, *path_strings)

Easy way to build a filter that uses full path strings delimited with ..

Takes an argument list of path strings.

from syrupy.filters import paths

def test_bar(snapshot):
    actual = {
      "date": datetime.now(),
      "list": [1,2,3],
    }
    assert actual == snapshot(exclude=paths("date", "list.1"))
# name: test_bar
  <class 'dict'> {
    'list': <class 'list'> [
      1,
      3,
    ],
  }
---

extension_class

This is a way to modify how the snapshot matches and serializes your data in a single assertion.

def test_foo(snapshot):
    actual_svg = "<svg></svg>"
    assert actual_svg == snapshot(extension_class=SVGImageSnapshotExtension)
Built-In Extensions

Syrupy comes with a few built-in preset configurations for you to choose from. You should also feel free to extend the AbstractSyrupyExtension if your project has a need not captured by one our built-ins.

  • AmberSnapshotExtension: This is the default extension which generates .ambr files. Serialization of most data types are supported.
    • Line control characters are normalised when snapshots are generated i.e. \r and \n characters are all written as \n. This is to allow interoperability of snapshots between operating systems that use disparate line control characters.
  • SingleFileSnapshotExtension: Unlike the AmberSnapshotExtension, which groups all tests within a single test file into a singular snapshot file, this extension creates one .raw file per test case.
  • PNGSnapshotExtension: An extension of single file, this should be used to produce .png files from a byte string.
  • SVGSnapshotExtension: Another extension of single file. This produces .svg files from an svg string.

Advanced Usage

By overriding the provided AbstractSnapshotExtension you can implement varied custom behaviours.

See examples of how syrupy can be used and extended in the test examples.

Extending Syrupy

Uninstalling

pip uninstall syrupy

If you have decided not to use Syrupy for your project after giving us a try, we'd love to get your feedback. Please create a GitHub issue if applicable, or drop a comment in our Discord server.

Contributing

Feel free to open a PR or GitHub issue. Contributions welcome!

To develop locally, clone this repository and run . script/bootstrap to install test dependencies. You can then use invoke --list to see available commands.

See contributing guide

Contributors


Noah

🚇 🤔 💻 📖 ⚠️

Emmanuel Ogbizi

💻 🎨 🚇 📖 ⚠️

Adam Lazzarato

📖

Marc Cataford

💻 ⚠️

Michael Rose

💻 ⚠️

Jimmy Jia

💻 ⚠️

Steven Loria

🚇

Artur Balabanov

💻

Huon Wilson

💻 🐛

Elizabeth Culbertson

💻 ⚠️

Joakim Nordling

🐛

This section is automatically generated via tagging the all-contributors bot in a PR:

@all-contributors please add <username> for <contribution type>

License

Syrupy is licensed under Apache License Version 2.0.

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

syrupy-1.4.7.tar.gz (2.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

syrupy-1.4.7-py3-none-any.whl (36.2 kB view details)

Uploaded Python 3

File details

Details for the file syrupy-1.4.7.tar.gz.

File metadata

  • Download URL: syrupy-1.4.7.tar.gz
  • Upload date:
  • Size: 2.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.6.4 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.2 CPython/3.9.0

File hashes

Hashes for syrupy-1.4.7.tar.gz
Algorithm Hash digest
SHA256 fbedb9d29f69a08fe0a5876ccc76d3c1a52f4bb64aae74ac5238857aa0837986
MD5 c4ceb60ec764f1b42967b2c798bb497b
BLAKE2b-256 b60d7b7db06a05223d427d485a20d83216358349cb3881f09fdde6ab21b27f28

See more details on using hashes here.

File details

Details for the file syrupy-1.4.7-py3-none-any.whl.

File metadata

  • Download URL: syrupy-1.4.7-py3-none-any.whl
  • Upload date:
  • Size: 36.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.6.4 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.2 CPython/3.9.0

File hashes

Hashes for syrupy-1.4.7-py3-none-any.whl
Algorithm Hash digest
SHA256 5c4b96f222fb2716898f916b20d911811f16a295d0da11eb3846f15bbac2fa55
MD5 48903230a5a7acc92228ced35aa25c30
BLAKE2b-256 5e786f09111ba7e8acd0464673076439255cc7910be3e39c65821fb37df000f6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page