Adding fixtures and parameterized functions to PyTest

As we discussed in our post on getting started with Pytest, the framework allows us to re-use tests by making use of test fixtures and parameterized functions. In this post, we will learn how to do so.

We will build on our simple calculator example that we discussed in the previous post and add to it. Without further ado, let us get started.

Adding test fixtures

- Advertisement -

As we saw in our small example, there is some repetition in the two basic tests that we wrote:

# calculator_test.py
from calculator import add

def test_add():
    firstNumber = 2
    secondNumber = 1
    assert add(firstNumber, secondNumber) == 3

def test_add():
    firstNumber = 2
    secondNumber = 1
    assert subtract(firstNumber, secondNumber) == 1

This might seem trivial in this case, but it is one of the productivity enhancements we can get with Pytest. We can avoid repetition and set up some helper code that gets executed before any tests. These fixtures are the perfect places to initialize resources that will be needed by the tests.

We can create fixture functions by marking them using the @pytest.fixture decorator. This provides us a way of injecting dependencies into our tests via function parameters.

Functions that require these fixtures can then accept them as an argument. So our fixture definition will become:

import pytest

@pytest.fixture
def numbers():
    firstNumber=2
    secondNumber=1
    return [firstNumber,secondNumber]

Since a fixture is a function, we could have either defined two functions to return the two numbers, or we can combine them in a list and return it from one. And now, we can have access to this list in our test case, and our test simplifies to:

def test_add(numbers):
    assert add(numbers[0], numbers[1]) == 3

And for the subtract one, we can simply do:

def test_add(numbers):
    assert subtract(numbers[0], numbers[1]) == 1

A few things to note about fixtures:

  • We can define multiple fixtures for a single file. These can then be individually accessed by their names in the argument of the test function.
  • A test can request multiple fixtures.
  • Every test receives a newly initialized value of the variables declared in the fixture. The values are not re-used among tests.
  • Fixtures are accessible only among a particular file.
  • Fixtures can request other fixtures.

Conftest.py

We might want to re-use some of our fixtures across files as well. We can do so by creating a file named conftest.py in our directory. The tests defined in this file are available to all the tests in the directory.

We can even have multiple conftest.py files in a project, but only one per Python module. Tests defined in a module can use all fixtures defined in the configuration for that module as well as those that are defined globally.

Scopes

By default, all fixtures are executed once per function. This can be an expensive operation if we are doing some intensive operation in our fixture. Every time an argument of a test has a fixture, that fixture gets executed.

If we were calling an API, for instance, we will make the request every time, slowing down our tests. We can define scopes for fixtures which gives us the capability to re-use fixture instances in between tests.

The possible values of scope are: functionclassmodulepackage or session.

# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
   return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

The smtp object will be re-used across the module.

Parameterized test functions

When it comes to writing tests, we usually want to test the function with multiple values. And more often than not, we end up writing multiple test cases for doing that. Pytest provides us with a feature known as parameterized test functions which enables testing of different scenarios in a single function.

We use the @pytest.mark.parameterize decorator. Before getting into the implementation, let us first understand what markers are.

Markers

Markers are decorators that allow classification of tests or give us the ability to perform a specific action. Some of the relevant markers arE:

@pytest.mark.skip(reason=None):
skip the given test function with an optional reason

@pytest.mark.filterwarnings(warning):
add a warning filter to the given test

@pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False):
mark the test function as an expected failure if eval(condition) has a True value

@pytest.mark.parametrize(argnames, argvalues):
call a test function multiple times passing in different arguments in turn.
argvalues generally needs to be a list of values
if argnames specifies only one name
or a list of tuples of values if argnames specifies multiple names.

To see a list of all markers, we can use:

pytest --markers

Writing parameterized functions

As we found out, we can parameterize our test cases using markers. Let us assume we wanted to test not just positive numbers, but negative numbers as well. Instead of writing multiple tests, we would instead use:

# calculator_test.py
from calculator import add

@pytest.mark.parametrize("firstNumber,secondNumber,expected", [
    (1, 2, 3),
    (2, -2, 0),
])
def test_add():
    assert add(firstNumber, secondNumber) == expected

This provides us a succinct way of combining values and avoiding repetition.

Conclusion

And that wraps up our exploration of fixtures and parameterized functions in Pytest. Now go and write some awesome tests! And if you have any questions, feel free to drop a comment below.

Recent Articles

TypeScript: the difference between interface and type

Once we start exploring TypeScript, we start using interfaces and types without really understanding the differences between them. I am guilty of...

Mutant: An open-source, cross-platform, encrypted programming language

Mutant is an open-source, cross-platform, compiled, garbage collected, encrypted programming language that wants to make secure programming and security research more accessible....

Writing better conditional expressions in JavaScript

Writing conditional expressions is pretty easy to do. But there is room for improvement in the way we have been doing it....

My career decision: choosing passion over money

As you might know, I recently switched jobs. In my long journey of finding a new one, there was a moment where...

Parsing and validating data in Python using Pydantic

Introduction of type hinting opened the gates for a lot of great new features in Python. And data validation and parsing became...

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here

Hi there! Want some more knowledge?

Think that the knowledge shared is helpful? You might want to give our mailing list a try. We'll send you 2-4 emails a month, right when new posts come out.

Hi there! Want some more knowledge?

Think that the knowledge shared is helpful? You might want to give our mailing list a try. We'll send you 2-4 emails a month, right when new posts come out.