Basic pytest teardown that runs on test completion - pytest

I am minimally using pytest as a generic test runner for large automated integration tests against various API products at work, and I've been trying to find an equally generic example of a teardown function that runs on completion of any test, regardless of success or failure.
My typical use pattern is super linear and usually goes something like this:
def test_1():
<logic>
assert something
def test_2():
<logic>
assert something
def test_3():
<logic>
assert something
Occasionally, when it makes sense to do so, at the top of my script I toss in a setup fixture with an autouse argument set to "True" that runs on the launch of every script:
#pytest.fixture(scope="session", autouse=True)
def setup_something():
testhelper = TestHelper
testhelper.create_something(host="somehost", channel="somechannel")
def test_1():
<logic>
assert something
def test_2():
<logic>
assert something
def test_3():
<logic>
assert something
Up until recently, disposable docker environments have allowed me to get away with skipping the entire teardown process, but I'm in a bit of pinch where one of those is not available right now. Ideally, without diverting from the same linear pattern I've already been using, how would I implement another pytest fixture that does something like:
#pytest.fixture
def teardown():
testhelper = TestHelper
testhelper.delete_something(thing=something)
when the run is completed?

Every fixture may have a tear down part:
#pytest.fixture
def something(request):
# setup code
def finalize():
# teardown code
request.addfinalizer(finalize)
return fixture_result
Or as I usually use it:
#pytest.fixture
def something():
# setup code
yield fixture_result
# teardown code
Note that in pytest pre-3.0, the decorator required for the latter idiom was #pytest.yield_fixture. Since 3.0, however, one can just use the regular #pytest.fixture decorator, and #pytest.yield_fixture is deprecated.
See more here

you can use these functions in your conftest.py
def pytest_runtest_setup(item):
pass
def pytest_runtest_teardown(item):
pass
see here for docs

Related

Varying parameters for a session-scoped pytest fixture

I have a test suite with an expensive fixture (it spins up a bunch of containers in a cluster), so I'd like to use a session-scoped fixture for it. However, it's configurable on several axes, and different subsets of tests need to test different subsets of the configuration space.
Here's a minimal demonstration of what I'm trying to do. By default tests need to test the combinations x=1,y=10 and x=2,y=10, but the tests in test_foo.py need to test x=3,y=10 so override the x fixture:
conftest.py:
import pytest
#pytest.fixture(scope="session", params=[1, 2])
def x(request):
return request.param
#pytest.fixture(scope="session", params=[10])
def y(request):
return request.param
#pytest.fixture(scope="session")
def expensive(x, y):
return f"expensive[{x}, {y}]"
test_bar.py:
def test_bar(expensive):
assert expensive in {"expensive[1, 10]", "expensive[2, 10]"}
test_foo.py:
import pytest
#pytest.fixture(scope="session", params=[3])
def x(request):
return request.param
def test_foo(expensive):
assert expensive in {"expensive[3, 10]"}
When I run this, I get the following:
test_bar.py::test_bar[1-10] PASSED [ 33%]
test_foo.py::test_foo[3-10] FAILED [ 66%]
test_bar.py::test_bar[2-10] PASSED [100%]
=================================== FAILURES ===================================
________________________________ test_foo[3-10] ________________________________
expensive = 'expensive[1, 10]'
def test_foo(expensive):
> assert expensive in {"expensive[3, 10]"}
E AssertionError: assert 'expensive[1, 10]' in {'expensive[3, 10]'}
It appears to have reused the 1-10 fixture from test_bar for the 3-10 test in test_foo. Is that expected (some sort of matching by position in the parameter list rather than value), or a bug in pytest? Is there some way I can get it to do what I'm aiming for?
Incidentally, if I make x in test_foo.py non-parametric (just returning a hard-coded 3) it also fails, but in a slightly different way: it runs both test_bar tests first, then reuses the second fixture for the test_foo test.
The problem here is that the expansive fixture is in session scope, and the parameters are only read once.
A workaround would be to move expansive to module scope, so it will be evaluated for each module. That would work, but it would evaluate the fixture for each test module, even if several of them use the same parameters. If you have several test modules that use the same parameters, you could use 2 separate fixtures with different scope instead, with the common code moved out, e.g:
import pytest
...
def do_expansive_stuff(a, b): # a and b are normal arguments, not fixtures
# setup containers with parameters a, b...
return f"expensive[{a}, {b}]"
#pytest.fixture(scope="session")
def expensive(x, y):
yield do_expansive_stuff(x, y)
#pytest.fixture(scope="module")
def expensive_module(x, y):
yield do_expansive_stuff(x, y)
You would use expensive_module in test_foo. This is of course assuming that the expansive setup has to be done separately for each parameter set. The downside of this approach is of course having to use differently named fixtures.
It would be nice if someone would come up with a cleaner approach...

How do I use a different fixture implementation based on a command line argument?

I am testing an app that has user profiles.
Normally, I tear down the profile after each test,
but it is very slow, so I wanted to have the option to run the test faster via
keeping the profile but tearing down changes after each test.
This is what I have now, and it works fine:
#pytest.fixture(scope="session")
def session_scope_app():
with empty_app_started() as app:
yield app
#pytest.fixture(scope="session")
def session_scope_app_with_profile_loaded(session_scope_app):
with profile_loaded(session_scope_app):
yield session_scope_app
if TEAR_DOWN_PROFILE_AFTER_EACH_TEST:
#pytest.fixture
def setup(session_scope_app):
with profile_loaded(session_scope_app):
yield session_scope_app
else:
#pytest.fixture
def setup(session_scope_app_with_profile_loaded):
with profile_state_preserved(session_scope_app_with_profile_loaded):
yield session_scope_app_with_profile_loaded
This produces a fixture setup that, as far as other tests are concerned,
behaves the same way regardless of whether the profile is torn down after each test.
Now, I want to turn TEAR_DOWN_PROFILE_AFTER_EACH_TEST into a command line
option. How can I do this? Command line options are not yet available in test collection stage,
and I can't just put the if into the fixture function body, as the two variants of setup depend on different fixtures.
There are two ways of doing that, but first, let's add the command option itself.
def pytest_addoption(parser):
parser.addoption("--tear-down-profile-after-each-test",
action="store_true",
default=True)
parser.addoption("--no-tear-down-profile-after-each-test", "-T",
action="store_false",
dest="tear_down_profile_after_each_test")
Now, we can either invoke fixtures dynamically, or create a tiny plugin that shuffles our fixtures.
Invoke the fixture dynamically
This is very simple. Instead of depending on a fixture via function arguments,
we can call request.getfixturevalue(name) from inside the fixture.
#pytest.fixture
def setup(session_scope_app):
if request.config.option.tear_down_profile_after_each_test:
with profile_loaded(session_scope_app):
yield session_scope_app
else:
session = request.getfixturevalue(
session_scope_app_with_profile_loaded.__name__
)
with profile_state_preserved(session):
yield session
(It's ok to depend on session_scope_app since session_scope_app_with_profile_loaded depends on it anyway.)
Pros: PyCharm is happy. Cons: you won't be seeing session_scope_app_with_profile_loaded in --setup-plan.
Make a simple plugin
Plugins have the benefit of having access to the configuration.
def pytest_configure(config):
class Plugin:
if config.option.tear_down_profile_after_each_test:
#pytest.fixture
def setup(self, session_scope_app):
with profile_loaded(session_scope_app):
yield session_scope_app
else:
#pytest.fixture
def setup(self, session_scope_app_with_profile_loaded):
with profile_state_preserved(session_scope_app_with_profile_loaded):
yield session_scope_app_with_profile_loaded
config.pluginmanager.register(Plugin())
Pros: You get excellent --setup-plan. Cons: PyCharm won't recongize that setup is a fixture.

Pytest session fixtures not executed anymore

For some time I ran a test suite with the following fixtures in the highest order conftest.py. This file contains fixtures that have to be available for each test in the test suite. They are building on each other. One fixture is requiring the other. Thus the execution order is implicitly clear. The fixtures basically yield objects required for connections.
#pytest.fixture(scope="session")
def A():
yield A
#pytest.fixture(scope="session")
def B(A):
yield B
#pytest.fixture(scope="session")
def C(B):
Then at one moment the setup did not work anymore. Only the first fixture was executed.
#pytest.fixture(scope="session")
def A():
yield A
We are currently trying to check what exactly may have been changed, that changed the behavior. We tried around with a pytest version change, changes in the pytest.ini file or init.py files. So far we have not found any reason for the changed behaviour.
Does anyone have a hint?
Adding the autouse=True argument made the fixtures work again.
#pytest.fixture(scope="session", autouse=True)
def A():
yield A
#pytest.fixture(scope="session", autouse=True)
def B(A):
yield B
#pytest.fixture(scope="session", autouse=True)
def C(B):

pytest fixture for certain test cases

Having a testclass and testcases like below:
class TestSomething():
...
#pytest.fixture(autouse=True)
def before_and_after_testcases(self):
setup()
yield
cleanup()
def test_abc_1():
...
def test_abc_2():
...
def test_def_1():
...
def test_def_2():
...
Problem is, before_and_after_testcases() would run for each testcase in the test class. Is it possible to let the fixture apply to testcases with abc pattern in the function name only? The fixture is not supposed to run on test_def_xxx, but I don't know how to exclude those testcases.
The autouse=True fixture is automatically applied to all of the tests, to remove that auto-application you'll remove autouse=True
but now that fixture isn't applied to any!
to manually apply that fixture to the tests that need it you can either:
add that fixture's name as a parameter (if you need the value that the fixture has)
decorate the tests which need that fixture with #pytest.mark.usefixtures('fixture_name_here')
Another approach is to split your one test class into multiple test classes, grouping the tests which need the particular auto-used fixtures
disclaimer: I'm a pytest developer, though I don't think that's entirely relevant to this answer SO just requires disclosure of affiliation

py.test mixing fixtures and asyncio coroutines

I am building some tests for python3 code using py.test. The code accesses a Postgresql Database using aiopg (Asyncio based interface to postgres).
My main expectations:
Every test case should have access to a new asyncio event loop.
A test that runs too long will stop with a timeout exception.
Every test case should have access to a database connection.
I don't want to repeat myself when writing the test cases.
Using py.test fixtures I can get pretty close to what I want, but I still have to repeat myself a bit in every asynchronous test case.
This is how my code looks like:
#pytest.fixture(scope='function')
def tloop(request):
# This fixture is responsible for getting a new event loop
# for every test, and close it when the test ends.
...
def run_timeout(cor,loop,timeout=ASYNC_TEST_TIMEOUT):
"""
Run a given coroutine with timeout.
"""
task_with_timeout = asyncio.wait_for(cor,timeout)
try:
loop.run_until_complete(task_with_timeout)
except futures.TimeoutError:
# Timeout:
raise ExceptAsyncTestTimeout()
#pytest.fixture(scope='module')
def clean_test_db(request):
# Empty the test database.
...
#pytest.fixture(scope='function')
def udb(request,clean_test_db,tloop):
# Obtain a connection to the database using aiopg
# (That's why we need tloop here).
...
# An example for a test:
def test_insert_user(tloop,udb):
#asyncio.coroutine
def insert_user():
# Do user insertion here ...
yield from udb.insert_new_user(...
...
run_timeout(insert_user(),tloop)
I can live with the solution that I have so far, but it can get cumbersome to define an inner coroutine and add the run_timeout line for every asynchronous test that I write.
I want my tests to look somewhat like this:
#some_magic_decorator
def test_insert_user(udb):
# Do user insertion here ...
yield from udb.insert_new_user(...
...
I attempted to create such a decorator in some elegant way, but failed. More generally, if my test looks like:
#some_magic_decorator
def my_test(arg1,arg2,...,arg_n):
...
Then the produced function (After the decorator is applied) should be:
def my_test_wrapper(tloop,arg1,arg2,...,arg_n):
run_timeout(my_test(),tloop)
Note that some of my tests use other fixtures (besides udb for example), and those fixtures must show up as arguments to the produced function, or else py.test will not invoke them.
I tried using both wrapt and decorator python modules to create such a magic decorator, however it seems like both of those modules help me create a function with a signature identical to my_test, which is not a good solution in this case.
This can probably solved using eval or a similar hack, but I was wondering if there is something elegant that I'm missing here.
I’m currently trying to solve a similar problem. Here’s what I’ve come up with so far. It seems to work but needs some clean-up:
# tests/test_foo.py
import asyncio
#asyncio.coroutine
def test_coro(loop):
yield from asyncio.sleep(0.1)
assert 0
# tests/conftest.py
import asyncio
#pytest.yield_fixture
def loop():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
yield loop
loop.close()
def pytest_pycollect_makeitem(collector, name, obj):
"""Collect asyncio coroutines as normal functions, not as generators."""
if asyncio.iscoroutinefunction(obj):
return list(collector._genfunctions(name, obj))
def pytest_pyfunc_call(pyfuncitem):
"""If ``pyfuncitem.obj`` is an asyncio coroutinefunction, execute it via
the event loop instead of calling it directly."""
testfunction = pyfuncitem.obj
if not asyncio.iscoroutinefunction(testfunction):
return
# Copied from _pytest/python.py:pytest_pyfunc_call()
funcargs = pyfuncitem.funcargs
testargs = {}
for arg in pyfuncitem._fixtureinfo.argnames:
testargs[arg] = funcargs[arg]
coro = testfunction(**testargs) # Will no execute the test yet!
# Run the coro in the event loop
loop = testargs.get('loop', asyncio.get_event_loop())
loop.run_until_complete(coro)
return True # TODO: What to return here?
So I basically let pytest collect asyncio coroutines like normal functions. I also intercept text exectuion for functions. If the to-be-tested function is a coroutine, I execute it in the event loop. It works with or without a fixture creating a new event loop instance per test.
Edit: According to Ronny Pfannschmidt, something like this will be added to pytest after the 2.7 release. :-)
Every test case should have access to a new asyncio event loop.
The test suite of asyncio uses unittest.TestCase. It uses setUp() method to create a new event loop. addCleanup(loop.close) is close automatically the event loop, even on error.
Sorry, I don't know how to write this with py.test if you don't want to use TestCase. But if I remember correctly, py.test supports unittest.TestCase.
A test that runs too long will stop with a timeout exception.
You can use loop.call_later() with a function which raises a BaseException as a watch dog.