This article assumes that you already have pytest
running and you can basically interact with your computer's command interface. If you are just starting out, you may need to start with the following articles.
In this article, you will learn how to write and run test functions with pytest
in a practical approach.
Writing test Functions
To Pytest
, test functions are basically functions whose names begins with "test_"
Pytest
automatically detects and executes these functions.
For the test functions to be automatically discovered, they should be placed in files whose names match the following patterns.
test_*.py
*_test.py
This means that a test file must either begin with test_
or end with _test
.
For example, assume we have a directory called project
, with the following structure.
project
├── areas.py
└── test_areas.py
#areas.py def rectangle_area(length, width): area = length * width return area
To write simple tests for the rectangle_area()
function, we will define a function test_rectangle_area()
in the test_areas.py
file.
#test_areas.py from areas import rectangle_area def test_rectangle_area(): assert rectangle_area(3, 4) == 12 assert rectangle_area(6, 8) == 48 assert rectangle_area(10, 10) == 100
In the test_rectangle_function()
above, we used the basic assert
statement to check whether the value returned by the rectangle_area()
function is equivalent to an expected value. We will talk about assert
statements in a while, for now let us focus on the structure of the test function.
To run the above tests just type pytest
and click enter in your computers command-line interface from the project
directory.
================================================= test session starts =================================================
platform win32 -- Python 3.13.2, pytest-8.3.4, pluggy-1.5.0
rootdir: C:\Users\Administrator\Desktop\project
collected 1 item
test_areas.py . [100%]
================================================== 1 passed in 0.26s ==================================================
In the above case, all our assertions are assertions evalueates to True
, thus the test passes. In case where a test fails, Pytest
provides detailed information about the failure, including the line number and the expected vs actual values.
Assert statements.
In Pytest
, the native assert
statement is the primary tools for writing assertions. This simplicity is one of the main reasons why pytest is so popular among developers.
for more details see:
In other test frameworks including unittest
which exists in the standard library, assert helper functions are normally used to create assertions. Pytest
, on the other hand, intercepts the basic assert
statements making it more convenient and easy to achieve the same.
The following table contrasts some basic assertions in pytest
and their equivalent in unittest
to demonstrate how Pytest
simplifies testing with native assert
statement
pytest | unittest |
---|---|
assert a == b |
.assertEqual(a, b) |
assert a != b |
.assertNotEqual(a, b) |
assert a is True |
.assertTrue(a) |
The assert
statement basically has the following syntax.
assert <expression>
expression
can be any valid Python expression, it should evaluate to a boolean value i.e True
or False
. If the expression evaluates to True
, the assertion passes, if False
the assertion fails.
In our test_rectangle_area()
function, we created some assertion statements. e.g
assert rectangle_area(3, 4) == 12
In this case the assertion passes, because the value that is returned when we call rectangle_area(3, 4)
is 12
. If the returned value is not equal to the expected value, the assertion fails and consequently, the entire test fails. Let us deliberately make the test_rectangle_area()
to fail to understand this better.
#test_areas.py from areas import rectangle_area def test_rectangle_area(): assert rectangle_area(3, 4) == 15 #assertion fails assert rectangle_area(6, 8) == 48 assert rectangle_area(10, 10) == 100
If you make the modifications and then call pytest
you will see that the entire test fails because that single assertion did not pass.
platform win32 -- Python 3.13.2, pytest-8.3.4, pluggy-1.5.0
rootdir: C:\Users\Administrator\Desktop\project
collected 1 item
test_areas.py F [100%]
====================================================== FAILURES =======================================================
_________________________________________________ test_rectangle_area _________________________________________________
def test_rectangle_area():
> assert rectangle_area(3, 4) == 15
E assert 12 == 15
E + where 12 = rectangle_area(3, 4)
test_areas.py:4: AssertionError
=============================================== short test summary info ===============================================
FAILED test_areas.py::test_rectangle_area - assert 12 == 15
================================================== 1 failed in 1.50s ==================================================
For each failing test, the line which caused the failure is shown with a >
, you can see this from the above output.
Expecting exceptions
In some cases, we may expect an exception to be raised. Pytests allows us to test that the exception is actually raised using the .raises()
function. This function can be used as a context manager meaning that we can use it in a with
statement.
If you look at our rectangle_area()
function in areas.py
, you will realize that it will work even when the length
or width
is negative, but mathematically, a rectangle with negative dimensions is undefined. Let us improve the function so that a ValueError
is raised in such a scenario.
#areas.py def rectangle_area(length, width): if width <= 0 or length <= 0: raise ValueError('Invalid rectangle dimensions.') area = length * width return area
Let us now add a test in test_rectangle_area()
function that will check that the function actually raises an error if it is called with invalid values.
from areas import rectangle_area import pytest def test_rectangle_area(): assert rectangle_area(3, 4) == 12 assert rectangle_area(6, 8) == 48 assert rectangle_area(10, 10) == 100 with pytest.raises(ValueError): rectangle_area(3, -4)
Pytest
will expect that the code inside the with
block to raise the exception that is given as argument. in the above case, the test will only pass if a ValueError
, is raised. If no error is raised, the test fails. if a different type of exception is raised, test fails.
If you call pytest
now, you will see that the test passes.
platform win32 -- Python 3.13.2, pytest-8.3.4, pluggy-1.5.0
rootdir: C:\Users\Administrator\Desktop
collected 1 item
project\test_areas.py . [100%]
================================================== 1 passed in 0.39s ==================================================
Try altering the with
block in test_rectangle_area()
function to see what happens in various scenarios. For example, you can call the rectangle_area()
with a non-negative valid values for width
and length
in which case the test will fail.
Marking tests
Marking tests allows you to control the execution of test function. This is achieved using the pytest.mark
decorators. You can run the following command to see all the builtin marker decorators.
pytest --markers
The following is a list of some of the built-in markers, that we will look at in this article.
@pytest.mark.skip
@pytest.mark.skipif
@pytest.mark.xfail
@pytest.mark.parametrize
Skipping test functions
For varying reasons, we may want to skip some test functions so that they are not executed by pytest
. Basically, we can achieve this using the @pytest.mark.skip
marker. This decorator makes the decorated function to be skipped unconditionally.
The skip marker has the following syntax.
@pytest.mark.skip(reason = None)
Where, reason is an optional text indicating why the function was skipped.
We will add more code to areas.py
file to demonstrate this.
#areas.py import math def rectangle_area(length, width): if width <= 0 or length <= 0: raise ValueError('Invalid rectangle dimensions.') area = length * width return area def circle_area(r): if r <= 0: raise ValueError(f'a circle with radius {r} is undefined.') are = math.pi * r * r return area
Let us add a test function for the circle_area()
function.
#test_areas.py from areas import rectangle_area, circle_area import pytest @pytest.mark.skip(reason = 'Already Tested'). def test_rectangle_area(): assert rectangle_area(3, 4) == 12 assert rectangle_area(6, 8) == 48 assert rectangle_area(10, 10) == 100 with pytest.raises(ValueError): rectangle_area(3, -4) def test_circle_area(): assert circle_area(7) == pytest.approx(153.86, 0.001 ) with pytest.raises(ValueError): circle_area(-7)
We have added the test_circle_area()
function. The older test_rectangle_area()
is decorated with the @pytest.mark.skip
decorator. If you call pytest
after updating the files, you will see that the test_rectangle_area()
function will be skipped . The output will be similar to one shown below.
================================================= test session starts ================================================= platform win32 -- Python 3.13.2, pytest-8.3.4, pluggy-1.5.0 rootdir: C:\Users\Administrator\Desktop collected 2 items project\test_areas.py s. [100%] ============================================ 1 passed, 1 skipped in 0.37s =============================================
Skipping tests conditionally
The @pytest.mark.skipif
decorator can be used to skip tests conditionally. This means that a test function will only be skipped if specified condition evaluates to True.
The syntax of this decorator is as follows.
@pytest.mark.skipif(condition, reason = None)
condition
is any valid Python expression, it should evaluate to a boolean value i.e True
or False
. If True
, the function is skipped, if False
, the function is executed normally.
Marking test as expected to fail
When you use skip
or skipif
decorators, the tests are not executed at all. We can use the @pytest.mark.xfail
decorator to tell pytest
to execute the test function but we expect that its tests will fail.
In the output, the decorated function is indicated with xfailed
if it actually failed as expected or xpassed
if it unexpectedly passes.
#test_areas.py from areas import rectangle_area, circle_area import pytest @pytest.mark.xfail def test_rectangle_area(): assert rectangle_area(3, 4) == 12 assert rectangle_area(6, 8) == 48 assert rectangle_area(10, 10) == 100 assert rectangle_area(3, -4) == -12 def test_circle_area(): assert circle_area(7) == pytest.approx(153.86, 0.001 ) with pytest.raises(ValueError): circle_area(-7)
In the above example, we expect the test_rectangle_area()
test to fail because of the last assertion.
If you run pytest
after the modification, you will get the following output.
================================================= test session starts =================================================
platform win32 -- Python 3.13.2, pytest-8.3.4, pluggy-1.5.0
rootdir: C:\Users\Administrator\Desktop
collected 2 items
project\test_areas.py x. [100%]
============================================ 1 passed, 1 xfailed in 1.32s =============================================
Parametrized Testing
Test parametrization is one of the most powerful features of Pytest
. It is used to achieve what is known as data-driven testing. Basically, it allows you to run the same test function with multiple set of inputs and expected outputs. This removes duplication and makes the tests more manageable.
To parametrize a test function, we use the @pytest.mark.parametrize
decorator.
The decorator takes in two arguments:
- a string containing the comma-separated parameter names
- a list of tuples, where each tuple represents a set of inputs.
To better understand this, let us remove redundancy in the test_rectangle_area()
function by parametrizing it.
#test_areas.py from areas import rectangle_area import pytest @pytest.mark.parametrize('length, width, expected', [ (3, 4, 12), (6, 8, 48), (10, 10, 100), (-3, -4, 12) ] ) def test_rectangle_area(length, width, expected): if length <= 0 or width <= 0 : with pytest.raises(ValueError): rectangle_area(length, width) else: assert rectangle_area(length, width) == expected
In the test function above, the first argument to the parametrize decorator is a string containing the parameter names separated with a comma i.e 'length, width, expected'
. The second argument is a list of tuples, where each tuple is an individual instance of the three parameters, i.e length
, width
and expected
.
The decorated test function i.e test_rectangle_area()
defines the three parameters.When pytest
is runs, it calls the test function with the individual instances from the tuples given in the decorator.
As you can see, this approach is more flexible and clean than defining redundant assert
statements inside the test function.
If you call pytest
after the modification, the output will be as follows.
================================================= test session starts =================================================
platform win32 -- Python 3.13.2, pytest-8.3.4, pluggy-1.5.0
rootdir: C:\Users\Administrator\Desktop\project
collected 4 items
test_areas.py .... [100%]
================================================== 4 passed in 0.37s ==================================================
Go through the parametrized test_rectangle_area()
function above to conceptualize the functionality of parametrization in pytest
.
As a task, create a parametrized version of the test_circle_area()
function.