'Pytest asserting fixture after teardown

I have a test that makes a thing, validates it, deletes the thing and confirms it was deleted.

def test_thing():
    thing = Thing()  # Simplified, it actually takes many lines to make a thing

    assert thing.exists

    thing.delete()  # Simplified, it also takes a few lines to delete it

    assert thing.deleted

Next I want to make many more tests that all use the thing, so it's a natural next step to move the thing creation/deletion into a fixture

@pytest.fixture
def thing():
    thing = Thing()  # Simplified, it actually takes many lines to make a thing
    yield thing
    thing.delete()  # Simplified, it also takes a few lines to delete it

def test_thing(thing):
    assert thing.exists

def test_thing_again(thing):
    # Do more stuff with thing

...

But now I've lost my assert thing.deleted.

I feel like I have a few options here, but none are satisfying.

  1. I could assert in the fixture but AFAIK it's bad practice to put assertions in the fixture because if it fails it will result in an ERROR instead of a FAIL.
  2. I could keep my original test and also create the fixture, but this would result in a lot of duplicated code for creating/deleting the thing.
  3. I can't call the fixture directly because I get a Fixture called directly exception so I could move the thing creation out into a generator that is used by both the fixture and the test. This feels clunky though and what happens if my thing fixture needs to use another fixture?

What is my best option here? Is there something I haven't thought of?



Solution 1:[1]

How about using the fixture where appropriate (to reduce duplication) but not where your logic only exists once.

@pytest.fixture
def thing():
    thing = Thing()  # Simplified, it actually takes many lines to make a thing
    yield thing
    thing.delete()  # Simplified, it also takes a few lines to delete it

def test_thing(thing):
    assert thing.exists

def test_thing_does_a_thing(thing):
    expected = "expected"
    assert thing.do_thing() == expected

def test_thing_deletes():
    # just don't use the fixture here
    thing = Thing()
    thing.delete()
    assert thing.deleted

Solution 2:[2]

Another solution could be to have a single fixture that yields a context manager, so the test can be in full control of invoking it.

@pytest.fixture
def gen_thing():

    @contextmanager
    def cm():
        thing = Thing()  # Simplified, it actually takes many lines to make a thing
        try:
            yield thing
        finally:
            thing.delete()  # Simplified, it also takes a few lines to delete it

    yield cm


def test_thing(gen_thing):
    with gen_thing() as thing:
        assert thing.exists
    assert thing.deleted


def test_thing_again(gen_thing):
    with gen_thing() as thing:
        # Do more stuff with thing

Creating the context manager as a closure means it would have the same scope as the fixture too.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 JJ Hassan
Solution 2 Jacob Tomlinson