Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - assert_match() with ignored keys #102

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ class APITestCase(TestCase):
If you want to update the snapshots automatically you can use the `python manage.py test --snapshot-update`.
Check the [Django example](https://github.com/syrusakbary/snapshottest/tree/master/examples/django_project).

### Ignoring dict keys
A common usecase for snapshot testing is to freeze an API by ensuring that it doesn't get changed unexpectedly.
Some data such as timestamps, UUIDs or similar random data will make your snapshots fail every time unless you mock these fields.

While mocking is a perfectly fine solution it might still not be the most time efficient and practical one.
Therefore `assert_match()` may take a keyword argument `ignore_keys`.
The values of any ignored key (on any nesting level) will not be compared with the snapshots value (but the key must still be present).


# Contributing

After cloning this repo and configuring a virtualenv for snapshottest (optional, but highly recommended), ensure dependencies are installed by running:
Expand Down
21 changes: 19 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,20 @@ If you want to update the snapshots automatically you can use the
``python manage.py test --snapshot-update``. Check the `Django
example <https://github.com/syrusakbary/snapshottest/tree/master/examples/django_project>`__.

Ignoring dict keys
~~~~~~~~~~~~~~~~~~

A common usecase for snapshot testing is to freeze an API by ensuring
that it doesn't get changed unexpectedly. Some data such as timestamps,
UUIDs or similar random data will make your snapshots fail every time
unless you mock these fields.

While mocking is a perfectly fine solution it might still not be the
most time efficient and practical one. Therefore ``assert_match()`` may
take a keyword argument ``ignore_keys``. The values of any ignored key
(on any nesting level) will not be compared with the snapshots value
(but the key must still be present).

Contributing
============

Expand All @@ -103,13 +117,16 @@ After developing, the full test suite can be evaluated by running:
# and
make test

If you change this ``README.md``, you'll need to have pandoc installed to update its ``README.rst`` counterpart (used by PyPI),
which can be done by running:
If you change this ``README.md``, remember to update its ``README.rst``
counterpart (used by PyPI), which can be done by running:

::

make README.rst

For this last step you'll need to have ``pandoc`` installed in your
machine.

Notes
=====

Expand Down
9 changes: 9 additions & 0 deletions examples/pytest/snapshots/snap_test_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@
snapshots['test_nested_objects frozenset'] = frozenset([
GenericRepr('#')
])

snapshots['test_snapshot_can_ignore_keys 1'] = {
'id': GenericRepr("UUID('fac2b49e-0ec1-407b-a840-3fbb0a522eb9')"),
'nested': {
'id': GenericRepr("UUID('1649c442-1fad-4b6d-9b14-5cf4ee9c929c')"),
'some_nested_key': 'some_nested_value'
},
'some_key': 'some_value'
}
16 changes: 16 additions & 0 deletions examples/pytest/test_demo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import uuid
from collections import defaultdict

from snapshottest.file import FileSnapshot
Expand Down Expand Up @@ -83,3 +84,18 @@ def test_nested_objects(snapshot):
snapshot.assert_match(tuple_, 'tuple')
snapshot.assert_match(set_, 'set')
snapshot.assert_match(frozenset_, 'frozenset')


def test_snapshot_can_ignore_keys(snapshot):
snapshot.assert_match(
{
"id": uuid.uuid4(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that single quotes are used for strings in other places, I'd stick to that.
See https://pep8.org/#string-quotes

In Python, single-quoted strings and double-quoted strings are the same. This PEP does not make a recommendation for this. Pick a rule and stick to it.

Copy link
Author

@HeyHugo HeyHugo Dec 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I've gotten so used to default black formatting so didn't notice. Agree this should be consistent, will change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm looking closer at the code I can see both double quotes and single quotes are used at this point in the project.

This is a problem in itself and should be addressed in a separate PR imo. Preferably with black formatting if you ask me since I think formatting beats linting. And at the same time linting this should be added to enforce it. (black --check if black i chosen)

I don't really care if it's single or double quote just consistent :)

"some_key": "some_value",
"nested":
{
"id": uuid.uuid4(),
"some_nested_key": "some_nested_value"
}
},
ignore_keys=("id",)
)
24 changes: 23 additions & 1 deletion snapshottest/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ class SnapshotTest(object):
def __init__(self):
self.curr_snapshot = ''
self.snapshot_counter = 1
self.ignore_keys = None

@property
def module(self):
Expand Down Expand Up @@ -225,14 +226,18 @@ def store(self, data):
self.module[self.test_name] = data

def assert_value_matches_snapshot(self, test_value, snapshot_value):
if self.ignore_keys is not None:
self.clear_ignore_keys(test_value)
self.clear_ignore_keys(snapshot_value)
formatter = Formatter.get_formatter(test_value)
formatter.assert_value_matches_snapshot(self, test_value, snapshot_value, Formatter())

def assert_equals(self, value, snapshot):
assert value == snapshot

def assert_match(self, value, name=''):
def assert_match(self, value, name='', ignore_keys=None):
self.curr_snapshot = name or self.snapshot_counter
self.ignore_keys = ignore_keys
self.visit()
if self.update:
self.store(value)
Expand All @@ -254,6 +259,23 @@ def assert_match(self, value, name=''):
def save_changes(self):
self.module.save()

def clear_ignore_keys(self, data):
if isinstance(data, dict):
for key, value in data.items():
if key in self.ignore_keys:
data[key] = None
else:
data[key] = self.clear_ignore_keys(value)
return data
elif isinstance(data, list):
for index, value in enumerate(data):
data[index] = self.clear_ignore_keys(value)
return data
elif isinstance(data, tuple):
return tuple(self.clear_ignore_keys(value) for value in data)

return data


def assert_match_snapshot(value, name=''):
if not SnapshotTest._current_tester:
Expand Down
4 changes: 2 additions & 2 deletions snapshottest/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def tearDown(self):
SnapshotTest._current_tester = None
self._snapshot = None

def assert_match_snapshot(self, value, name=''):
self._snapshot.assert_match(value, name=name)
def assert_match_snapshot(self, value, name='', ignore_keys=None):
self._snapshot.assert_match(value, name=name, ignore_keys=ignore_keys)

assertMatchSnapshot = assert_match_snapshot
65 changes: 65 additions & 0 deletions tests/test_snapshot_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import unicode_literals

import time

import pytest

from snapshottest.module import SnapshotModule, SnapshotTest
Expand Down Expand Up @@ -111,3 +113,66 @@ def test_snapshot_does_not_match_other_values(snapshot_test, value, other_value)
with pytest.raises(AssertionError):
snapshot_test.assert_match(other_value)
assert_snapshot_test_failed(snapshot_test)


SNAPSHOTABLE_DATA_FACTORIES = {
"dict": lambda: {"time": time.time(), "this key": "must match"},
"nested dict": lambda: {"nested": {"time": time.time(), "this key": "must match"}},
"dict in list": lambda: [{"time": time.time(), "this key": "must match"}],
"dict in tuple": lambda: ({"time": time.time(), "this key": "must match"},),
"dict in list in dict": lambda: {"list": [{"time": time.time(), "this key": "must match"}]},
"dict in tuple in dict": lambda: {"tuple": ({"time": time.time(), "this key": "must match"},)}
}


@pytest.mark.parametrize(
"data_factory",
[
pytest.param(data_factory)
for data_factory in SNAPSHOTABLE_DATA_FACTORIES.values()
], ids=list(SNAPSHOTABLE_DATA_FACTORIES.keys())
)
def test_snapshot_assert_match__matches_with_diffing_ignore_keys(
snapshot_test, data_factory
):
data = data_factory()
# first run stores the value as the snapshot
snapshot_test.assert_match(data)

# Assert with ignored keys should succeed
data = data_factory()
snapshot_test.reinitialize()
snapshot_test.assert_match(data, ignore_keys=("time",))
assert_snapshot_test_succeeded(snapshot_test)

# Assert without ignored key should raise
data = data_factory()
snapshot_test.reinitialize()
with pytest.raises(AssertionError):
snapshot_test.assert_match(data)


@pytest.mark.parametrize(
"existing_snapshot, new_snapshot",
[
pytest.param(
{"time": time.time(), "some_key": "some_value"},
{"some_key": "some_value"},
id="new_snapshot_missing_key",
),
pytest.param(
{"some_key": "some_value"},
{"time": time.time(), "some_key": "some_value"},
id="new_snapshot_extra_key",
),
],
)
def test_snapshot_assert_match_does_not_match_if_ignore_keys_not_present(
snapshot_test, existing_snapshot, new_snapshot
):
# first run stores the value as the snapshot
snapshot_test.assert_match(existing_snapshot)

snapshot_test.reinitialize()
with pytest.raises(AssertionError):
snapshot_test.assert_match(new_snapshot, ignore_keys=("time",))