Search code examples
pythonautomationpytestnose

pytest: get parametrize values during setup/teardown method


My automation framework uses pytest setup/teardown type of testing instead of fixtures. I also have several levels of classes:

BaseClass - highest, all tests inhriet from it

FeatureClass - medium, all tests related to the programs feature inherit from it

TestClass - hold the actual tests

edit, for examples sake, i change the DB calls to a simple print

I want to add DB report in all setup/teardowns. i.e. i want that the general BaseClass setup_method will create a DB entry for the test and teardown_method will alter the entry with the results. i have tried but i can't seem to get out of method the values of currently running test during run time. is it possible even? and if not, how could i do it otherwise?

samples: (in base.py)

class Base(object):

    test_number = 0
    def setup_method(self, method):
        Base.test_number += 1
        self.logger.info(color.Blue("STARTING TEST"))
        self.logger.info(color.Blue("Current Test: {}".format(method.__name__)))
        self.logger.info(color.Blue("Test Number: {}".format(self.test_number)))
        # --->here i'd like to do something with the actual test parameters<---
        self.logger.info("print parameters here")

    def teardown_method(self, method):
        self.logger.info(color.Blue("Current Test: {}".format(method.__name__)))
        self.logger.info(color.Blue("Test Number: {}".format(self.test_number)))
        self.logger.info(color.Blue("END OF TEST"))

(in my_feature.py)

class MyFeature(base.Base):

    def setup_method(self, method):
        # enable this feature in program
        return True

(in test_my_feature.py)

class TestClass(my_feature.MyFeature):

    @pytest.mark.parametrize("fragment_length", [1,5,10])    
    def test_my_first_test(self):
        # do stuff that is changed based on fragment_length
        assert verify_stuff(fragment_length)

so how can i get the parameters in setup_method, of the basic parent class of the testing framework?


Solution

  • The brief answer: NO, you cannot do this. And YES, you can work around it.


    A bit longer: these unittest-style setups & teardowns are done only for compatibility with the unittest-style tests. They do not support the pytest's fixture, which make pytest nice.

    Due to this, neither pytest nor pytest's unittest plugin provide the context for these setup/teardown methods. If you would have a request, function or some other contextual objects, you could get the fixture's values dynamically via request.getfuncargvalue('my_fixture_name').

    However, all you have is self/cls, and method as the test method object itself (i.e. not the pytest's node).

    If you look inside of the _pytest/unittest.py plugin, you will find this code:

    class TestCaseFunction(Function):
        _excinfo = None
    
        def setup(self):
            self._testcase = self.parent.obj(self.name)
            self._fix_unittest_skip_decorator()
            self._obj = getattr(self._testcase, self.name)
            if hasattr(self._testcase, 'setup_method'):
                self._testcase.setup_method(self._obj)
            if hasattr(self, "_request"):
                self._request._fillfixtures()
    

    First, note that the setup_method() is called fully isolated from the pytest's object (e.g. self as the test node).

    Second, note that the fixtures are prepared after the setup_method() is called. So even if you could access them, they will not be ready.

    So, generally, you cannot do this without some trickery.


    For the trickery, you have to define a pytest hook/hookwrapper once, and remember the pytest node being executed:

    conftest.py or any other plugin:

    import pytest
    
    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_protocol(item, nextitem):
        item.cls._item = item
        yield
    

    test_me.py:

    import pytest
    
    
    class Base(object):
        def setup_method(self, method):
            length = self._item.callspec.getparam('fragment_length')
            print(length)
    
    
    class MyFeature(Base):
        def setup_method(self, method):
            super().setup_method(method)
    
    
    class TestClass(MyFeature):
        @pytest.mark.parametrize("fragment_length", [1,5,10])    
        def test_my_first_test(self, fragment_length):
            # do stuff that is changed based on fragment_length
            assert True  # verify_stuff(fragment_length)
    

    Also note that MyFeature.setup_method() must call the parent's super(...).setup_method() for obvious reasons.

    The cls._item will be set on each callspec (i.e. each function call with each parameter). You can also put the item or the specific parameters into some other global state, if you wish.

    Also be carefull not to save the field in the item.instance. The instance of the class will be created later, and you have to use setup_instance/teardown_instance method for that. Otherwise, the saved instance's field is not preserved and is not available as self._item in setup_method().

    Here is the execution:

    ============ test session starts ============
    ......
    collected 3 items                                                                                                                                                                 
    
    test_me.py::TestClass::test_my_first_test[1] 1
    PASSED
    test_me.py::TestClass::test_my_first_test[5] 5
    PASSED
    test_me.py::TestClass::test_my_first_test[10] 10
    PASSED
    
    ============ 3 passed in 0.04 seconds ============