syrupy-project / syrupy

:pancakes: The sweeter pytest snapshot plugin
https://syrupy-project.github.io/syrupy/
Apache License 2.0
553 stars 36 forks source link

Can I reference the snapshot in another test? #914

Closed xyshell closed 4 days ago

xyshell commented 1 month ago

Hey there, thanks for working on this fantastic project! I've been using it both personally and in work.

One question is: Can we reference the snapshot in another test? pseudo-code to illustrate:

def test1(snapshot):
    result = compute(flavor='a')   # output some long string 
    assert res == snapshot

def test2(snapshot):
    result = compute(flavor='b')  # should output the same string as flavor='a'
    assert result == snapshot_in_test1  # I'd like to reference the snapshot in test1 and make assertion here

for now, I have to combine them together in one test, like:

def test(snapshot):
    result_a = compute(flavor='a')
    result_b = compute(flavor='b')
    assert result_a == result_b == snapshot

But this will make one test too long to be good.

Do we have such feature? I can't find it in the doc (but I may miss something).

noahnu commented 3 weeks ago

No that's not currently supported. It's create odd dependency issues off the top of my head. I.e. when updating a snapshot, which test case is the source of truth?

xyshell commented 3 weeks ago

No that's not currently supported. It's create odd dependency issues off the top of my head. I.e. when updating a snapshot, which test case is the source of truth?

Hey @noahnu. Thanks for giving it a thought. How about having a fixture to define the single source of truth?, like:

@pytest.fixture
def special_snapshot():
    return ... 

Then both tests can reference to the fixture, like:

def test1(special_snapshot):  # instead of the normal "snapshot"
    result = compute(flavor='a')
    assert res == special_snapshot

def test2(special_snapshot):  # instead of the normal "snapshot"
    result = compute(flavor='b')
    assert result == special_snapshot

When updating snapshot, there could be some logic to make sure all tests referencing the special_snapshot producing the same result and got updated together. Does this make sense to you? Happy to discuss more.

noahnu commented 4 days ago

Is the issue you're trying to solve that you want to avoid duplicate snapshots in the filesystem? Otherwise I'd probably just do something like:

@pytest.fixture(scope="module")
def some_computation():
  return compute(flavor="a")

def test1(some_computation, snapshot):
  assert some_computation == snapshot

def test2(some_computation):
  assert some_computation == compute(flavor="b")

The scope=module will ensure it's the same value between the 2 test cases. Only 1 test case would have the snapshot but you'd transitively get equality between all values.

To do this entirely in syrupy, I suppose you could hook into the default syrupy extension class (amber) and use the relevant methods to manually recall the snapshot data based on the name of the other function. I don't think this would be simple though.

We might be able to hack something together by patching the SnapshotAssertion class so the test location is optional, or it uses the first node it comes across, e.g.:

import pytest

from syrupy.assertion import SnapshotAssertion

COMPUTE_PRODUCES_SAME_VALUE = True

def compute(flavor):
    if COMPUTE_PRODUCES_SAME_VALUE:
        return "some-static-value"
    return f"computed-{flavor}"

@pytest.fixture(scope="module")
def stateful_snapshot(request: "pytest.FixtureRequest"):
    snapshot = SnapshotAssertion(
        update_snapshots=request.config.option.update_snapshots,
        extension_class=...,
        test_location=PyTestLocation(request.node), # TODO: What would this node be?
        session=request.session.config._syrupy,  # type: ignore
    )

    class StatefulSnapshot:
        assertions: int = 0

        def __eq__(self, other) -> bool:
            self.assertions += 1
            if self.assertions == 1:
                return snapshot(name="initial") == other
            return snapshot(diff="initial") == other

    return StatefulSnapshot()

def test1(stateful_snapshot):
    assert compute(flavor='a') == stateful_snapshot

def test2(stateful_snapshot):
    assert compute(flavor='b') == stateful_snapshot

ultimately I don't think this is feasible