Most Wanted
Most Wanted

Reputation: 7019

PyTest skip module_teardown()

I have following code in my tests module

def teardown_module():
    clean_database()
def test1(): pass
def test2(): assert 0

and I want teardown_module() (some cleanup code) to be called only if some test failed. Otherwise (if all passed) this code shouldn't have to be called. Can I do such a trick with PyTest?

Upvotes: 4

Views: 3380

Answers (2)

Joe Marley
Joe Marley

Reputation: 309

The answer posted here and link to documentation was helpful but not sufficient for my needs. I needed a module teardown function to execute for each module independently if any test in that module (.py) file failed.

A complete sample project is available on GitHub

To start with, we need a hook to attach the test function result to the test node. This is taken directly from the pytest docs:

# in conftest.py
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    # execute all other hooks to obtain the report object
    outcome = yield
    rep = outcome.get_result()
    # set a report attribute for each phase of a call, which can
    # be "setup", "call", "teardown"
    var_name = "rep_" + rep.when
    setattr(item, var_name, rep)

After that, we need another hook for the test case to find the module and store itself there, so the module can easily find its test cases. Perhaps there's a better way, but I was unable to find one.

# also in conftest.py
@pytest.fixture(scope="function", autouse=True)
def _testcase_exit(request):
    yield
    parent = request.node.parent
    while not isinstance(parent, pytest.Module):
        parent = parent.parent
    try:
        parent.test_nodes.append(request.node)
    except AttributeError:
        parent.test_nodes = [request.node]

Once we do that, it's nice to have a decorator function to have the module on completion look through its test nodes, find if there are any failures, and then if there were call the function associated with the decorator:

# also also in conftest.py
def module_error_teardown(f):
    @wraps(f)
    @pytest.fixture(scope="module", autouse=True)
    def wrapped(request, *args, **kwargs):
        yield
        try:
            test_nodes = request.node.test_nodes
        except AttributeError:
            test_nodes = []

        something_failed = False
        for x in test_nodes:
            try:
                something_failed |= x.rep_setup.failed
                something_failed |= x.rep_call.failed
                something_failed |= x.rep_teardown.failed
            except AttributeError:
                pass
        if something_failed:
            f(*args, **kwargs)
    return wrapped

Now we have all the necessary framework to work with. Now, a test file with a failing test case is easy to write:

from conftest import module_error_teardown


def test_something_that_fails():
    assert False, "Yes, it failed."


def test_something_else_that_fails():
    assert False, "It failed again."


@module_error_teardown
def _this_gets_called_at_the_end_if_any_test_in_this_file_fails():
    print('')
    print("Here's where we would do module-level cleanup!")

Upvotes: 0

ntki
ntki

Reputation: 2304

You can. But it is a little bit of a hack. As written here: http://pytest.org/latest/example/simple.html#making-test-result-information-available-in-fixtures you do the following, to set up an attribute for saving the status of each phase of the testcall:

# content of conftest.py
import pytest
@pytest.mark.tryfirst
def pytest_runtest_makereport(item, call, __multicall__):
    rep = __multicall__.execute()
    setattr(item, "rep_" + rep.when, rep)
    return rep

and in the fixture you just examine the condition on those attributes like this:

import pytest
@pytest.yield_fixture(scope="module", autouse=True)
def myfixture(request):
    print "SETUP"
    yield
    # probably should not use "_collected" to iterate over test functions
    if any(call.rep_call.outcome != "passed" for call in request.node._collected):
        print "TEARDOWN"

This way if any of the tests associated with that module fixture is not "passed" (so "failed" or "skipped") then the condition holds.

Upvotes: 3

Related Questions