简体   繁体   中英

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?

If you want to test that a "thing" is deleted, make a fixture without teardown, delete it in the test, then assert if it is deleted.

@pytest.fixture
def thing_create():
    # Perform all the creation steps
    thing = Thing()
    ...

    yield thing


def thing_delete(thing):
    # Perform all the deletion steps
    ...
    thing.delete()  


@pytest.fixture
def thing_all(thing_create):
    yield thing_create
    thing_delete(thing_create)


def test_thing(thing_all):
    assert thing_all.exists


def test_thing_again(thing_create):
    thing_delete(thing_create)
    assert thing_create.deleted

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM