Skip to content

Commit

Permalink
Feature - assert_match with ignored keys
Browse files Browse the repository at this point in the history
Enable setting ignored_keys argument on snapshot.assert_match()
ignored_keys is a list or tuple of keys whose values should be ignored when running assert_match. The values of the ignored keys are set to None, this way we still assert that the keys are present but ignore its value. Dicts, lists and tuples are traversed recursively to ignore any occurrence of the key for all dicts on any level.
  • Loading branch information
HeyHugo committed Oct 26, 2019
1 parent 4ac2b4f commit c6ecc69
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 5 deletions.
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 `ignored_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 ``ignored_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(),
"some_key": "some_value",
"nested":
{
"id": uuid.uuid4(),
"some_nested_key": "some_nested_value"
}
},
ignored_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.ignored_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.ignored_keys is not None:
self.clear_ignored_keys(test_value)
self.clear_ignored_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='', ignored_keys=None):
self.curr_snapshot = name or self.snapshot_counter
self.ignored_keys = ignored_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_ignored_keys(self, data):
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, dict):
data[key] = self.clear_ignored_keys(value)
if key in self.ignored_keys:
data[key] = None
return data
elif isinstance(data, list):
for index, value in enumerate(data):
data[index] = self.clear_ignored_keys(value)
return data
elif isinstance(data, tuple):
return tuple(self.clear_ignored_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='', ignored_keys=None):
self._snapshot.assert_match(value, name=name, ignored_keys=ignored_keys)

assertMatchSnapshot = assert_match_snapshot
63 changes: 63 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,64 @@ 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 = [
lambda: {"time": time.time(), "this key": "must match"},
lambda: {"nested": {"time": time.time(), "this key": "must match"}},
lambda: [{"time": time.time(), "this key": "must match"}],
lambda: ({"time": time.time(), "this key": "must match"},),
]


@pytest.mark.parametrize(
"data_factory",
[
pytest.param(data_factory)
for data_factory in SNAPSHOTABLE_DATA_FACTORIES
],
)
def test_snapshot_assert_match__matches_with_diffing_ignored_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, ignored_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_ignored_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, ignored_keys=("time",))

0 comments on commit c6ecc69

Please sign in to comment.