Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

🥞 The sweeter pytest snapshot plugin

License

NotificationsYou must be signed in to change notification settings

syrupy-project/syrupy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Logo

All ContributorsStagecodecov

Pytest>=5.1.0,<9.0.0PypiWheelPyPI - Python VersionPyPI - DownloadsPyPI - License

Overview

Syrupy is a zero-dependencypytest 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

Pytest and Python Compatibility

Syrupy will always be compatible with the latest version of Python and Pytest. If you're running an old version of Python or Pytest, you will need to use an older major version of Syrupy:

Syrupy VersionPython SupportPytest Support
4.x.x>3.8.1>=7
3.x.x>=3.7, <4>=5.1, <8
2.x.x>=3.6, <4>=5.1, <8

Usage

Basic Usage

In a pytest test filetest_file.py:

deftest_foo(snapshot):actual="Some computed value!"assertactual==snapshot

when you runpytest, 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 astest_file.py. The__snapshots__ directory and all its children should be committed along with your test code.

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(...)"

If you need bypass a custom object representation to use the amber standard, it is easy using the following helpers.

deftest_object_as_named_tuple(snapshot):assertsnapshot==AmberDataSerializer.object_as_named_tuple(obj_with_custom_repr)

Seetest_snapshot_object_as_named_tuple_class for an example on automatically doing this for all nested properties

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 snapshotexclude configuration option.

deflimit_foo_attrs(prop,path):allowed_foo_attrs= {"do","not","serialize","these","attrs"}returnisinstance(path[-1][1],Foo)andpropinallowed_foo_attrsdeftest_bar(snapshot):actual=Foo(...)assertactual==snapshot(exclude=limit_foo_attrs)

B. Provide a filter function to the snapshotinclude configuration option.

deflimit_foo_attrs(prop,path):allowed_foo_attrs= {"only","serialize","these","attrs"}returnisinstance(path[-1][1],Foo)andpropinallowed_foo_attrsdeftest_bar(snapshot):actual=Foo(...)assertactual==snapshot(include=limit_foo_attrs)

C. Or override the__dir__ implementation to control the attribute list.

classFoo:def__dir__(self):return ["only","serialize","these","attrs"]deftest_bar(snapshot):actual=Foo(...)assertactual==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 topytest by the plugin.

OptionDescriptionDefault
--snapshot-updateSnapshots will be updated to match assertions and unused snapshots will be deleted.False
--snapshot-detailsIncludes details of unused, generated, and updated snapshots (test name and snapshot location) in the final report.False
--snapshot-warn-unusedPrints a warning on unused snapshots rather than fail the test suite.False
--snapshot-default-extensionUse to change the default snapshot extension class.AmberSnapshotExtension
--snapshot-no-colorsDisable test results output highlighting. Equivalent to setting the environment variablesANSI_COLORS_DISABLED orNO_COLORDisabled by default if not in terminal.
--snapshot-patch-pycharm-diffOverride PyCharm's default diffs viewer when looking at snapshot diffs. SeeIDE IntegrationsFalse
--snapshot-diff-modeConfigures how diffs are displayed on assertion failure. If working with very large snapshots, disabling the diff can improve performance.detailed
--snapshot-ignore-file-extensionsComma separated list of file extensions to ignore when walking the file tree and discovering used/unused snapshots.No extensions are ignored by default.

Assertion Options

These are the options available on thesnapshot assertion fixture.Use of these options are one shot and do not persist across assertions.For more persistent options seeadvanced 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.

ArgumentDescription
dataCurrent serializable value being matched on
pathOrdered 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.

Composing Matchers

Multiple matchers can be composed together usingmatchers, e.g.:

fromsyrupy.matchersimportcompose_matchersdeftest_multiple_matchers(snapshot):data= {"number":1,"datetime":datetime.datetime.now(),"float":1.3    }assertdata==snapshot(matcher=compose_matchers(path_type(types=(int,float),replacer=lambda*_:"MATCHER_1"),path_type(types=(datetime.datetime,),replacer=lambda*_:"MATCHER_2"),        ),    )
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 aValueError if the types specified are not matched.

ArgumentDescription
mappingDict of path string to tuples of class types, including primitives e.g. (MyClass, UUID, datetime, int, str)
typesTuple of class types used if none of the path strings from the mapping are matched
strictIf a path is matched but the value at the path does not match one of the class types in the tuple then aPathTypeError is raised
regexIf true, themapping key is treated as a regular expression when matching paths
replacerCalled with any matched value and result is used as the replacement that is serialized. Defaults to the object type when not given
fromsyrupy.matchersimportpath_typedeftest_bar(snapshot):actual= {"date_created":datetime.now(),"value":"Some computed value!!",    }assertactual==snapshot(matcher=path_type({"date_created": (datetime,),"nested.path.id": (int,),    }))
# name: test_bardict({'date_created':datetime,'value':'Some computed value!!',  })# ---

NOTE: Whenregex isTrue all matcher mappings are treated as regex patterns

path_value(mapping=None, *, **kwargs)

Shares the samekwargs aspath_type matcher, with the exception of themapping argument type.Only runs replacement for objects at a matching path where the value of the mapping also matches the object data string repr.

ArgumentDescription
mappingDict of path string to object value string representations

Seetest_regex_matcher_str_value for example usage.

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 returntrue if the property should be excluded, orfalse if the property should be included.

ArgumentDescription
propCurrent property on the object, could be any hashable value that can be used to retrieve a value e.g.1,"prop_str",SomeHashableObject
pathOrdered 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.

fromsyrupy.filtersimportpropsdeftest_bar(snapshot):actual= {"id":uuid.uuid4(),"list": [1,2,3],    }assertactual==snapshot(exclude=props("id","1"))
# name: test_bardict({'list':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.

fromsyrupy.filtersimportpathsdeftest_bar(snapshot):actual= {"date":datetime.now(),"list": [1,2,3],    }assertactual==snapshot(exclude=paths("date","list.1"))
# name: test_bardict({'list':list([1,3,    ]),  })# ---

include

This allows you filter an object's properties to a subset using a predicate. This is the opposite ofexclude. All the same property filters supporterd byexclude are supported forinclude.

The include parameter takes a filter function that accepts two keyword arguments.It should returntrue if the property should be include, orfalse if the property should not be included.

ArgumentDescription
propCurrent property on the object, could be any hashable value that can be used to retrieve a value e.g.1,"prop_str",SomeHashableObject
pathOrdered path traversed to the current value e.g.(("a", dict), ("b", dict)) from{ "a": { "b": { "c": 1 } } }}

Note thatinclude has some caveats which make it a bit more difficult to use thanexclude. Bothinclude andexclude are evaluated for each key of an object before traversing down nested paths. This means if you want to include a nested path, you must include all parents of the nested path, otherwise the nested child will never be reached to be evaluated against the include predicate. For example:

obj= {"nested": {"key":True }}assertobj==snapshot(include=paths("nested","nested.key"))

The extra "nested" is required, otherwise the nested dictionary will never be searched -- it'd get pruned too early.

To avoid adding duplicate path parts, we provide a convenientpaths_include which supports a list/tuple instead of a string for each path to match:

obj= {"other":False,"nested": {"key":True }}assertobj==snapshot(include=paths_include(["other"], ["nested","key"]))

extension_class

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

deftest_foo(snapshot):actual_svg="<svg></svg>"assertactual_svg==snapshot(extension_class=SVGImageSnapshotExtension)

diff

This is an option to snapshot only the diff between the actual object and a previous snapshot, with thediff argument being the previous snapshotindex/name.

deftest_diff(snapshot):actual0= [1,2,3,4]actual1= [0,1,3,4]assertactual0==snapshotassertactual1==snapshot(diff=0)# This is equivalent to the lines above# Must use the index name to diff when givenassertactual0==snapshot(name='snap_name')assertactual1==snapshot(diff='snap_name')
Built-In Extensions

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

Amber Extensions

  • 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.
  • SingleFileAmberSnapshotExtension: A variant of theAmberSnapshotExtension which writes 1 snapshot per file.

Other Formats

  • SingleFileSnapshotExtension: This extension creates one.raw file per test case. Note that the default behaviour of the SingleFileSnapshotExtension is to write raw bytes to disk. There is no further "serialization" that happens. TheSingleFileSnapshotExtension is mostly used as a building block for other extensions such as the image extensions, the JSON extension, as well as theSingleFileAmberSnapshotExtension extension. In the default "binary" mode, attempting to serialize a non-byte-like object will throw a TypeError.
  • PNGImageSnapshotExtension: An extension of single file, this should be used to produce.png files from a byte string.
  • SVGImageSnapshotExtension: Another extension of single file. This produces.svg files from an svg string.
  • JSONSnapshotExtension: Another extension of single file. This produces.json files from dictionaries and lists.

name

By default, if you make multiple snapshot assertions within a single test case, an auto-increment identifier will be used to index the snapshots. You can override this behaviour by specifying a custom snapshot name to use in place of the auto-increment number.

deftest_case(snapshot):assert"actual"==snapshot(name="case_a")assert"other"==snapshot(name="case_b")

Warning: If you use a custom name, you must make sure the name is not re-used within a test case.

Advanced Usage

By overriding the providedAbstractSyrupyExtension you can implement varied custom behaviours.

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

Overriding defaults

It is possible to overrideinclude,exclude,matchers andextension_class on a more global level just once,instead of every time per test. By default, after every assertion the modified values per snapshot assert are revertedto their default values. However, it is possible to override those default values with ones you would like persisted,which will be treated as the new defaults.

To achieve that you can usesnapshot.with_defaults, which will create new instance ofSnapshotAssertion with the provided values.

snapshot.use_extension is retained for compatibility. However, it is limited to only overriding the default extension class.

JSONSnapshotExtension

This extension can be useful when testing API responses, or when you have to deal with long dictionaries that are cumbersome to validate inside a test. For example:

importpytestfromsyrupy.extensions.jsonimportJSONSnapshotExtension@pytest.fixturedefsnapshot_json(snapshot):returnsnapshot.with_defaults(extension_class=JSONSnapshotExtension)# or return snapshot.use_extension(JSONSnapshotExtension)deftest_api_call(client,snapshot_json):resp=client.post("/endpoint")assertresp.status_code==200assertsnapshot_json==resp.json()

API responses often contain dynamic data, like IDs or dates. You can still validate and store other data of a response by leveraging syrupy matchers. For example:

fromdatetimeimportdatetimefromsyrupy.matchersimportpath_typedeftest_api_call(client,snapshot_json):resp=client.post("/user",json={"name":"Jane"})assertresp.status_code==201matcher=path_type({"id": (int,),"registeredAt": (datetime,)    })assertsnapshot_json(matcher=matcher)==resp.json()

The generated snapshot:

{"id":"<class 'int'>","registeredAt":"<class 'datetime'>","name":"Jane"}

Or a case where the value needs to be replaced using a condition e.g. file path string

importrefromsyrupy.matchersimportpath_typedeftest_matches_generated_string_value(snapshot,tmp_file):matcher=path_value(mapping={"file_path":r"\w+://(.*/)+dir/filename.txt"},replacer=lambda_,match:match[0].replace(match[1],"<tmp-file-path>/"),types=(str,),    )assertsnapshot(matcher=matcher)==tmp_file

The generated snapshot:

{"name":"Temp Files","file_path":"scheme://<tmp-file-path>/dir/filename.txt"}

Ignoring File Extensions (e.g. DVC Integration)

If using a tool such asDVC or other tool where you need to ignore files by file extension, you can update yourpytest.ini like so:

[pytest]addopts = --snapshot-ignore-file-extensions dvc

A comma separated list is supported, like so:

[pytest]addopts = --snapshot-ignore-file-extensions dvc,tmp,zip

Extending Syrupy

Inline Snapshots

Syrupy does not support inline snapshots. For inline snapshots, we recommend checking out theinline-snapshot library.

IDE Integrations

PyCharm

ThePyCharm IDE comes with a built-in tool for visualizing differences between expected and actual results in a test. To properly render Syrupy snapshots in the PyCharm diff viewer, we need to apply a patch to the diff viewer library. To do this, use the--snapshot-patch-pycharm-diff flag, e.g.:

In yourpytest.ini:

[pytest]addopts = --snapshot-patch-pycharm-diff

See#675 for the original issue.

Known Limitations

  • pytest-xdist support only partially exists. There is no issue when it comes to reads however when you attempt to runpytest --snapshot-update, if running with more than 1 process, the ability to detect unused snapshots is disabled. See#535 for more information.

We welcome contributions to patch these known limitations.

Uninstalling

pipuninstallsyrupy

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.

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 useinvoke --list to see available commands.

See contributingguide

Contributors

Noah
Noah

🚇🤔💻📖⚠️
Emmanuel Ogbizi
Emmanuel Ogbizi

💻🎨🚇📖⚠️
Adam Lazzarato
Adam Lazzarato

📖
Marc Cataford
Marc Cataford

💻⚠️
Michael Rose
Michael Rose

💻⚠️
Jimmy Jia
Jimmy Jia

💻⚠️
Steven Loria
Steven Loria

🚇
Artur Balabanov
Artur Balabanov

💻
Huon Wilson
Huon Wilson

💻🐛
Elizabeth Culbertson
Elizabeth Culbertson

💻⚠️
Joakim Nordling
Joakim Nordling

🐛
Ouail
Ouail

💻
Denis
Denis

💻
N0124
N0124

💻
dtczest
dtczest

🐛
Eddie Darling
Eddie Darling

📖
darrenburns
darrenburns

📖
Magnus Heskestad Waage
Magnus Heskestad Waage

🐛
Herbert Ho
Herbert Ho

🐛
Tolga Eren
Tolga Eren

🐛
John Kurkowski
John Kurkowski

🐛
Atharva Arya
Atharva Arya

💻
Michał Jelonek
Michał Jelonek

💻
ManiacDC
ManiacDC

💻
Dmitry Dygalo
Dmitry Dygalo

📖
Allan Chain
Allan Chain

🐛
Nir Schulman
Nir Schulman

💻
Joost Lekkerkerker
Joost Lekkerkerker

💻
epenet
epenet

⚠️
Tom Sparrow
Tom Sparrow

🐛
Samy Laumonier
Samy Laumonier

🐛

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 underApache License Version 2.0.


[8]ページ先頭

©2009-2025 Movatter.jp