NashTech Blog

Table of Contents

What fixtures are

Fixtures define the steps and data that constitute the arrange phase of a test. In pytest, they are functions that define a test’s act phase; this is a powerful technique for designing more complex tests.

Tests can depend on as many fixtures as you want, and fixtures can use other fixtures, as well.

Fixture scopes

Fixtures are created when first requested by a test, and are destroyed based on their scope:

  • function: the default scope, the fixture is destroyed at the end of the test.
  • class: the fixture is destroyed during teardown of the last test in the class.
  • module: the fixture is destroyed during teardown of the last test in the module.
  • package: the fixture is destroyed during teardown of the last test in the package where the fixture is defined, including sub-packages and sub-directories within it.
  • session: the fixture is destroyed at the end of the test session.

How to use

We can tell pytest that a particular function is a fixture by decorating it with @pytest.fixture.

Requesting fixtures

Test functions request fixtures they require by declaring them as arguments. When pytest goes to run a test, it looks at the parameters in that test function’s signature, and then searches for fixtures that have the same names as those parameters. Once pytest finds them, it runs those fixtures, captures what they returned (if anything), and passes those objects into the test function as arguments.

import pytest

@pytest.fixture
def fruit_bowl(): # fixture function
    return ["apple", "banana"]

def test_fruit_salad(fruit_bowl):
    assert fruit_bowl == ["apple", "banana"] # fruit_bowl contains the results after running fixture function

In this example, test_fruit_saladrequestsfruit_bowl (i.e. def test_fruit_salad(fruit_bowl):), and when pytest sees this, it will execute the fruit_bowl fixture function and pass the object it returns into test_fruit_salad as the fruit_bowl argument.

Setup fixtures

Sometimes test functions do not directly need access to a fixture object. For example, tests may require some setup steps before the main steps (like opening the browser and connecting storage..). We separate the creation of the fixture into a conftest.py file 

@pytest.fixture(scope="class")
def setup(request):
    global driver
    browser_name = request.config.getoption("browser_name")
    if browser_name == "chrome":
        driver = webdriver.Chrome(ChromeDriverManager().install())
    elif browser_name == "firefox":
        driver = webdriver.Firefox(executable_path=GeckoDriverManager().install())

    driver.implicitly_wait(5)
    driver.get("https://google.com/")
    driver.maximize_window()
    request.cls.driver = driver
    yield
    driver.close()

and declare its use in a test module via a usefixtures marker, then the setup fixture will be required for the execution of each test method:

@pytest.mark.usefixtures("setup")
class TestHomePage():
    def test_loginSuccess(self, getData):
        homepage= HomePage(self.driver)
        homepage.getUsername().send_keys(getData["username"])
        homepage.getPassword().send_keys(getData["password"])
        homepage.submitForm().click()
        alertText = homepage.getSuccessMessage().text
        assert ("Success" in alertText)

    def test_loginFail(self, getData):
        homepage= HomePage(self.driver)
        homepage.getUsername().send_keys(getData["username"])
        homepage.getPassword().send_keys(getData["password"])
        homepage.submitForm().click()
        alertText = homepage.getFailMessage().text
        assert ("Fail" in alertText)

conftest.py help to sharing fixtures across multiple files. This file serves as a means of providing fixtures for an entire directory. Fixtures defined in a conftest.py can be used by any test in that package without needing to import them (pytest will automatically discover them). You can have multiple nested directories/packages containing your tests, and each directory can have its own conftest.py with its own fixtures, adding on to the ones provided by the conftest.py files in parent directories.

Yield fixtures

“Yield” fixtures yield instead of return. With these fixtures, we can run some code and pass an object back to the requesting fixture/test, just like with the other fixtures. The only differences are:

  1. return is swapped out for yield.
  2. Any teardown code for that fixture is placed after the yield.

Once pytest figures out a linear order for the fixtures, it will run each one up until it returns or yields, and then move on to the next fixture in the list to do the same thing.

Once the test is finished, pytest will go back down the list of fixtures, but in the reverse order, taking each one that yielded, and running the code inside it that was after the yield statement.

@pytest.fixture(scope="class")
def setup(request):
    global driver
    browser_name = request.config.getoption("browser_name")
    if browser_name == "chrome":
        driver = webdriver.Chrome(ChromeDriverManager().install())
    elif browser_name == "firefox":
        driver = webdriver.Firefox(executable_path=GeckoDriverManager().install())

    driver.implicitly_wait(5)
    driver.get("https://google.com/")
    driver.maximize_window()
    request.cls.driver = driver # store driver to class variable to use across the requesting test context, because we cannot return before/after yield block
    yield
    driver.close()
Data-driven fixtures

pytest.fixture() allows one to parametrize fixture functions. Fixture functions can be parametrized in which case they will be called multiple times, each time executing the set of dependent tests. Test functions usually do not need to be aware of their re-running. Fixture parametrization helps to write exhaustive functional tests for components which themselves can be configured in multiple ways.

The main change is the declaration of params with @pytest.fixture, a list of values for each of which the fixture function will execute and can access a value via request.param

@pytest.fixture(params=[{"username":"tester1", "password":"123456"},{"username":"tester2", "password":"123456"}])
def getData(self, request):
    return request.param

@pytest.fixture(params=HomePageData.getTestData("TestCase2"))
def getData(self, request):
    return request.param

and declare its use in a test module as arguments to access the dataset

@pytest.mark.usefixtures("setup")
class TestHomePage():
    def test_login1(self, getData):
        homepage= HomePage(self.driver)
        homepage.getUsername().send_keys(getData["username"])
        homepage.getPassword().send_keys(getData["password"])
        homepage.submitForm().click()
        alertText = homepage.getSuccessMessage().text
        assert ("Success" in alertText)

As in the previous example, we can flag the fixture to create getData fixture instances which will cause all tests using the fixture to run twice. 

Conclusion

Pytest Fixtures are important in testing because they provide a reliable, repeatable and consistent context for tests. This can help make tests more efficient and easier to maintain as well. Fixtures also help to reduce code duplication by allowing you to reuse common setup/teardown logic and manage/share test data and resources across multiple tests. 

Reference: About fixtures — pytest documentation

 

Picture of Dung Nguyen

Dung Nguyen

I'm be here as a Senior Automation Test Engineer for nearly 10 years. I've experienced on many automation frameworks and tools like Selenium with variety of programming languages, Cypress, RobotFW, TestComplete...I also play more on performance API testing using Jmeter.

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top