Run code before and after each test in py.test?
Asked Answered
C

8

183

I want to run additional setup and teardown checks before and after each test in my test suite. I've looked at fixtures but not sure on whether they are the correct approach. I need to run the setup code prior to each test and I need to run the teardown checks after each test.

My use-case is checking for code that doesn't cleanup correctly: it leaves temporary files. In my setup, I will check the files and in the teardown I also check the files. If there are extra files I want the test to fail.

Coburn answered 25/3, 2014 at 7:9 Comment(0)
W
230

py.test fixtures are a technically adequate method to achieve your purpose.

You just need to define a fixture like that:

@pytest.fixture(autouse=True)
def run_around_tests():
    # Code that will run before your test, for example:
    files_before = # ... do something to check the existing files
    # A test function will be run at this point
    yield
    # Code that will run after your test, for example:
    files_after = # ... do something to check the existing files
    assert files_before == files_after

By declaring your fixture with autouse=True, it will be automatically invoked for each test function defined in the same module.

That said, there is one caveat. Asserting at setup/teardown is a controversial practice. I'm under the impression that the py.test main authors do not like it (I do not like it either, so that may colour my own perception), so you might run into some problems or rough edges as you go forward.

Wymore answered 25/3, 2014 at 15:12 Comment(5)
assertions should not be done inside fixtures. It is not a best practice.Lair
really nice answerServetnick
How to run before fixtures? not just before testsPreshrunk
@CHINTANVADGAMA Why not assert in fixtures? Can you point the uninformed to some blog or article detailing the best practices and why?Chrisy
The key moment here is using yield, not autouse=True as I understand it. I used it to mock fastapi dependency as fixture and it worked well.Wallsend
R
79

You can use a fixture in order to achieve what you want.

import pytest

@pytest.fixture(autouse=True)
def run_before_and_after_tests(tmpdir):
    """Fixture to execute asserts before and after a test is run"""
    # Setup: fill with any logic you want

    yield # this is where the testing happens

    # Teardown : fill with any logic you want

Detailed Explanation

  1. @pytest.fixture(autouse=True), from the docs: "Occasionally, you may want to have fixtures get invoked automatically without declaring a function argument explicitly or a usefixtures decorator." Therefore, this fixture will run every time a test is executed.

  2. # Setup: fill with any logic you want, this logic will be executed before every test is actually run. In your case, you can add your assert statements that will be executed before the actual test.

  3. yield, as indicated in the comment, this is where testing happens

  4. # Teardown : fill with any logic you want, this logic will be executed after every test. This logic is guaranteed to run regardless of what happens during the tests.

Note: in pytest there is a difference between a failing test and an error while executing a test. A Failure indicates that the test failed in some way. An Error indicates that you couldn't get to the point of doing a proper test.

Consider the following examples:

Assert fails before test is run -> ERROR

import pytest


@pytest.fixture(autouse=True)
def run_around_tests():
    assert False # This will generate an error when running tests
    yield
    assert True

def test():
    assert True

Assert fails after test is run -> ERROR

import pytest


@pytest.fixture(autouse=True)
def run_around_tests():
    assert True
    yield
    assert False

def test():
    assert True

Test fails -> FAILED

import pytest


@pytest.fixture(autouse=True)
def run_around_tests():
    assert True
    yield
    assert True

def test():
    assert False

Test passes -> PASSED

import pytest


@pytest.fixture(autouse=True)
def run_around_tests():
    assert True
    yield
    assert True

def test():
    assert True
Reenareenforce answered 7/7, 2020 at 22:11 Comment(1)
Can you access the result of the test in the logic after yield? What about variables created within the test's scope? If so, how?Chrisy
C
18

Fixtures are exactly what you want. That's what they are designed for.

Whether you use pytest style fixtures, or setup and teardown (module, class, or method level) xUnit style fixtures, depends on the circumstance and personal taste.

From what you are describing, it seems like you could use pytest autouse fixtures.
Or xUnit style function level setup_function()/teardown_function().

Pytest has you completely covered. So much so that perhaps it's a fire hose of information.

Convivial answered 19/8, 2015 at 22:18 Comment(0)
I
14

You can use Module level setup/teardown Fixtures of Pytest.

Here's the Link

http://pytest.org/latest/xunit_setup.html

It Works as follows:

 def setup_module(module):
     """ setup any state specific to the execution of the given module."""

 def teardown_module(module):
     """ teardown any state that was previously setup with a setup_module
     method."""

 Test_Class():
        def test_01():
          #test 1 Code

It will call setup_module before this test and teardown_module after test completes.

You can include this fixture in each test-script to run it for each test.

IF you want to use something that is common to all tests in a directory You can use package/directory level fixtures nose framework

http://pythontesting.net/framework/nose/nose-fixture-reference/#package

In __init__.py file of the package you can include following

     def setup_package():
       '''Set up your environment for test package'''

     def teardown_package():
        '''revert the state '''
Irish answered 31/3, 2014 at 12:30 Comment(0)
F
6

You may use decorators but programatically, so you don't need to put the decorator in each method.

I'm assuming several things in next code:

The test methods are all named like: "testXXX()" The decorator is added to the same module where test methods are implemented.

def test1():
    print ("Testing hello world")

def test2():
    print ("Testing hello world 2")

#This is the decorator
class TestChecker(object):
    def __init__(self, testfn, *args, **kwargs):
        self.testfn = testfn

    def pretest(self):
        print ('precheck %s' % str(self.testfn))
    def posttest(self):
        print ('postcheck %s' % str(self.testfn))
    def __call__(self):
        self.pretest()
        self.testfn()
        self.posttest()


for fn in dir() :
    if fn.startswith('test'):
        locals()[fn] = TestChecker(locals()[fn])

Now if you call the test methods...

test1()
test2()

The output should be something like:

precheck <function test1 at 0x10078cc20>
Testing hello world
postcheck <function test1 at 0x10078cc20>
precheck <function test2 at 0x10078ccb0>
Testing hello world 2
postcheck <function test2 at 0x10078ccb0>

If you have test methods as class methods, the approach is also valid. For instance:

class TestClass(object):
    @classmethod
    def my_test(cls):
        print ("Testing from class method")

for fn in dir(TestClass) :
    if not fn.startswith('__'):
        setattr(TestClass, fn, TestChecker(getattr(TestClass, fn)))

The call to TestClass.my_test() will print:

precheck <bound method type.my_test of <class '__main__.TestClass'>>
Testing from class method 
postcheck <bound method type.my_test of <class '__main__.TestClass'>>
Foremast answered 25/3, 2014 at 13:30 Comment(3)
This looks like it might work for free fucntions. I also have class fucntions (though I am trying to get rid of all the test classes).Coburn
It also works for class method, I've updated my answer.Foremast
Anyone had success running decorators on a test function which has a fixture?Fetching
E
4

It is an old question but I personally found another way from the docs : Use the pytest.ini file :

[pytest]
usefixtures = my_setup_and_tear_down
import pytest

@pytest.fixture
def my_setup_and_tear_down():

    # SETUP
    # Write here the logic that you need for the setUp

    yield # this statement will let the tests execute

    # TEARDOWN 
    # Write here the logic that you need after each tests

About the yield statement and how it allows to run the test : HERE

Enneagon answered 27/10, 2021 at 14:7 Comment(1)
This is nice to know, but I think what's left out here is that for this to work, your fixture needs to be in the conftest.py file and if it is not in this file, you need a way to load fixtures in specific directories.Dealfish
P
0

Fixtures by default have scope=function. So, if you just use a definition such as

@pytest.fixture
def fixture_func(self)

It defaults to (scope='function').

So any finalizers in the fixture function will be called after each test.

Portsmouth answered 5/5, 2018 at 23:10 Comment(0)
S
0

Had a similar issue, but then for reloading urls in unit tests that are based on a setting. As this was one of the first hits, I leave this here for other devs. This thread inspired me to create this reusable solution. It's a pytest fixture that sets a setting before the start of the unit test, then reloads the root url and the client.urls (add more if you have more apps with dynamic urls based on the setting), yield (do the unit test) and everything after yield is the teardown. In the teardown I put everything back to how it was before. Meaning; turn off the feature flag, and reload the routes again. This way the routes do not leak into other unit tests.

I used Django3.2, pytest 7.2, and python 3.10

# contents of conftest.py (fixtures)

import sys

from importlib import import_module, reload

import pytest
from django.urls import clear_url_caches


def _reload_urls(root_urlconf: str):
    # first clear url cache
    clear_url_caches()

    # then reload the root url-config
    if root_urlconf in sys.modules:
       reload(import_module(root_urlconf))

    # then reload all the app specific urls (for now just one is enough)
    if "client.urls" in sys.modules:
        reload(import_module("client.urls"))


@pytest.fixture()
def use_legacy_donation_module(settings):
    # setup
    settings.PROJECT_USE_NEW_DONATION_MODULE = False
    _reload_urls(settings.ROOT_URLCONF)

    # unit test
    yield

   # teardown
   settings.PROJECT_USE_NEW_DONATION_MODULE = True
  _reload_urls(settings.ROOT_URLCONF)

An example unit test that loads different routes based on settings.PROJECT_USE_NEW_DONATION_MODULE

# contents unit_test.py

def test_renders_form(client, use_legacy_donation_module):
    response = client.get(reverse("donations:legacy_embed"))
    assert response.template_name == ["legacy_embed/form.html"]
Selfconfessed answered 7/9, 2023 at 18:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.