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

Support ignoring fields when serializing #149

Closed
iamogbz opened this issue Mar 4, 2020 · 7 comments · Fixed by #262
Closed

Support ignoring fields when serializing #149

iamogbz opened this issue Mar 4, 2020 · 7 comments · Fixed by #262
Labels
community Discussions or community related feature request New feature or request released

Comments

@iamogbz
Copy link
Collaborator

iamogbz commented Mar 4, 2020

Is your feature request related to a problem? Please describe.

When asserting on snapshots support filtering out properties that should not be matched on. This would allow deterministic snapshots when matching on objects that generate realtime values in tests without needing to mock.

Describe the solution you'd like

def test_my_dict(snapshot):
	my_dict = {
		"match_me": True,
		"do_not_match_me": time.time(),
		"nested_do_not_match": {
			"do_not_match_me": time.time(),
		},
	}
	assert my_dict == snapshot(exclude=("do_not_match_me",))
# name: test_my_dict
  <class 'dict'> {
    'match_me': True,
	'nested_do_not_match': <class 'dict'> {
	},
  }
---

Describe alternatives you've considered

This is already possible but is cumbersome to implement, and cannot easily be extended to other serialization types.

class OmitDataSerializer(DataSerializer):
	@classmethod
	def __filter_data(cls, data) -> Dict[str, Any]:
		return {
			key: data[key]
			for key in data
			if key not in ("do_not_match_me",)
		}
	
    @classmethod
    def serialize_dict(cls, data, **kwargs) -> str:
        return super().serialize_dict(cls.__filter_data(data), **kwargs)


class OmitDataExtension(AmberSnapshotExtension):
    def serialize(self, data):
        return OmitDataSerializer.serialize(data)


def test_my_dict(snapshot):
	my_dict = {
		"match_me": True,
		"do_not_match_me": time.time(),
		"nested_do_not_match": {
			"do_not_match_me": time.time(),
		},
	}
	assert my_dict == snapshot(extension=OmitDataExtension)

Additional context

@iamogbz iamogbz added the feature request New feature or request label Mar 4, 2020
@noahnu
Copy link
Collaborator

noahnu commented Mar 4, 2020

Thoughts on include and exclude instead of ignore?

We could support strings (literal property matchers) and predicates

@iamogbz
Copy link
Collaborator Author

iamogbz commented Mar 4, 2020

@noahnu I think I prefer exclude over ignore, although not sure about having an include as well

We could support strings (literal property matchers) and predicates

What would be the arguments to the predicate?

def predicate(field: str, value: Any, path: Tuple[str] = ()) -> bool:
    """
    field: current field name to be excluded
    value: current field value to be excluded
    path: fields traversed so far
    """

We could replace the bool return with Optional[Any] and only exclude if it's None otherwise replace with the new value

@iamogbz iamogbz added the community Discussions or community related label Mar 4, 2020
@noahnu
Copy link
Collaborator

noahnu commented Mar 8, 2020

We could replace the bool return with Optional[Any] and only exclude if it's None otherwise replace with the new value

That feels more like a "transformation" function. A bool should be sufficient, and if we support some sort of transformation, it would be in some other parameter (not exclude).

@taion
Copy link
Contributor

taion commented Apr 21, 2020

We use this sort of approach internally. I took a stab at porting what I had over into something more Syrupy-native, but for us there's a twist.

For non-deterministic values, we keep track on a per-test-case the number of unique values of that type we've seen.

For example, the first time we see a new UUID, we'll replace it with e.g. <UUID(index=1)>. If we see a second new UUID, we'll replace it with e.g. <UUID(index=2)>. If we see the first UUID again, though, we'll have <UUID(index=3)>.

In general I think this sort of approach lets you get slightly nicer assertions around even non-deterministic values, in their being at least the non-deterministic UUIDs or timestamps in ways that you expect.

What stopped me from implementing this in a reasonable way was that everything on DataSerializer was a class method, so I didn't have a nice way of keeping around the sort of per-test stated needed for this sort of thing.

@iamogbz
Copy link
Collaborator Author

iamogbz commented Apr 30, 2020

@taion while this is pending, would mocking the UUID you use with one that just increments each time and then resetting the value after each test case work?

e.g.

class MockUUID:
    value = 0

    def __init__(self):
        self.increment()

    @classmethod
    def increment(cls):
        cls.value += 1

    @classmethod
    def reset(cls):
        cls.value = 0

    def __repr__(self):
        return f"<UUID(index={self.value})>"


@pytest.fixture
def mock_uuid(monkeypatch):
    monkeypatch.setattr("src.path.to.module.uuid.uuid4", MockUUID)
    yield
    MockUUID.reset()


def test_non_deterministic_1(snapshot, mock_uuid):
    assert NonDeterministic() == snapshot
    assert NonDeterministic() == snapshot


def test_non_deterministic_2(snapshot, mock_uuid):
    assert NonDeterministic() == snapshot

@taion
Copy link
Contributor

taion commented Apr 30, 2020

That's neat – it could definitely work for some cases (though there are a few places where we generate v5 UUIDs off other external IDs coming in). To clarify, though, we're not blocked on this at all; we just run our custom "mask nondeterministic values" function on our data before asserting on the snapshot.

@syrupy-bot
Copy link
Contributor

🎉 This issue has been resolved in version 0.5.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
community Discussions or community related feature request New feature or request released
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants