Pytest Fixture and Setup/Teardown Methods

Pytest has two nice features: parametrization and fixtures. They serve completely different purposes, but you can use fixtures to do parametrization.

What is a fixture?

A fixture is a function, which is automatically called by Pytest when the name of the argument (argument of the test function or of the another fixture) matches the fixture name. In another words:

@pytest.fixture
def fixture1():
   return "Yes"
def test_add(fixture1):
    assert fixture1 == "Yes"

In this example fixture1 is called at the moment of execution of test_add. The return value of fixture1 is passed into test_add as an argument with a name fixture1. There are many, many nuances to fixtures (e.g. they have scope, they can use yield instead of return to have some cleanup code, etc, etc), but in this post we are looking into one and only one of those features—an argument named params to the pytest.fixture decorator. It is used for parametrization.

Let’s create a sample.py file which contains the following code:

import json


class StudentData:
    def __init__(self):
        self.__data = None

    def connect(self, data_file):
        with open(data_file) as json_file:
            self.__data = json.load(json_file)

    def get_data(self, name):
        for stu in self.__data['students']:
            if stu['name'] == name:
                return stu

Similarly, let’s create the test_sample.py file:

from sample import StudentData
import pytest


def test_scott_data():
    db = StudentData()
    db.connect('data.json')
    scott_data = db.get_data('Joseph')
    assert scott_data['id'] == 1
    assert scott_data['name'] == 'Joseph'
    assert scott_data['result'] == 'pass'


def test_mark_data():
    db = StudentData()
    db.connect('data.json')
    mark_data = db.get_data('Jaden')
    assert mark_data['id'] == 2
    assert mark_data['name'] == 'Jaden'
    assert mark_data['result'] == 'fail'

You can see, we have used a .json file, since here we tsting files along with databases. So, data.json file is as follows:

{
    "students": [
        {
            "id": 1,
            "name": "Joseph",
            "result": "pass"
        },
        {
            "id": 2,
            "name": "Jaden",
            "result": "fail"
        }
    ]
}

On testing our file we will get like this:

Although, our test cases have passed but it is not a not good practice to repeat our code which we can see in our test_sample.py file. So this can be solved by using two methods:

  • Setup/Teardown Method
  • Fixture

Setup/Teardown Method

  • This method falls under classic xunit-style setup. This section describes a classic and popular way how you can implement fixtures (setup and teardown test state) on a per-module/class/function basis. You must be familiar with this method if you know other testing frameworks like unittest or nose.
  • So make the changes the in test_sample.py file:
from sample import StudentData
import pytest
db = None


def setup_module(module):
    print('*****SETUP*****')
    global db
    db = StudentData()
    db.connect('data.json')


def teardown_module(module):
    print('******TEARDOWN******')
    db.close()


def test_scott_data():
    scott_data = db.get_data('Joseph')
    assert scott_data['id'] == 1
    assert scott_data['name'] == 'Joseph'
    assert scott_data['result'] == 'pass'


def test_mark_data():
    mark_data = db.get_data('Jaden')
    assert mark_data['id'] == 2
    assert mark_data['name'] == 'Jaden'
    assert mark_data['result'] == 'fail'
  • Let’s just create a dummy close function for our teardown method in sample.py. The code of sample.py is as follows:
import json


class StudentData:
    def __init__(self):
        self.__data = None

    def connect(self, data_file):
        with open(data_file) as json_file:
            self.__data = json.load(json_file)

    def get_data(self, name):
        for stu in self.__data['students']:
            if stu['name'] == name:
                return stu

    def close(self):
        pass
  • Now, let’s test our file:

Fixture

Fixtures are functions, which will run before each test function to which it is applied. Fixtures are used to feed some data to the tests such as database connections, URLs to test and some sort of input data. Therefore, instead of running the same code for every test, we can attach fixture function to the tests and it will run and return the data to the test before executing each test.

A function is marked as a fixture by:

@pytest.fixture

A test function can use a fixture by mentioning the fixture name as an input parameter.

So, let’s create the test_sample.py file:

from sample import StudentData
import pytest

@pytest.fixture(scope='module')
def db():
    print('*****SETUP*****')
    db = StudentData()
    db.connect('data.json')
    yield db
    print('******TEARDOWN******')
    db.close()


def test_scott_data(db):
    scott_data = db.get_data('Joseph')
    assert scott_data['id'] == 1
    assert scott_data['name'] == 'Joseph'
    assert scott_data['result'] == 'pass'


def test_mark_data(db):
    mark_data = db.get_data('Jaden')
    assert mark_data['id'] == 2
    assert mark_data['name'] == 'Jaden'
    assert mark_data['result'] == 'fail'
  • So after testing, it should show as follows:

Benefits of pytest fixtures

Right away we can see some cool benefits.

  • It’s obvious which tests are using a resource, as the resource is listed in the test param list.
  • I don’t have to artificially create classes (or move tests from one file to another) just to separate fixture usage.
  • The teardown code is tightly coupled with the setup code for one resource.
  • Scope for the lifetime of the resource is specified at the location of the resource setup code. This ends up being a huge benefit when you want to fiddle with scope to save time on testing. If everything starts going haywire, it’s a one line change to specify function scope, and have setup/teardown run around every function/method.
  • It’s less code. The pytest solution is smaller than the class solution.