pytest in 5 minutes

This post is a quick tutorial to pytest, an awesome testing framework for Python. The main goal of the tutorial is to show features of pytest that are most useful in day to day life. Who can benefit from this tutorial?

  • Experienced developer, who uses Python occasionally and needs to rewind the syntax and features of pytest.
  • A developer who never used pytest and wants to get started in less than 5 minutes.
  • A developer who is looking to fill in gaps and learn few tricks using pytest.

All code examples that are shown below can be found here. More exhaustive details on pytest can be found in pytest documentation.

How to get started?

It’s extremely simple, just install pytest from PyPI.

pip install pytest

or

python -m pip install pytest

How to run tests?

Most likely you will have pytest in your path, so to run it’s as simple as

pytest

In this case you may need to add conftest.py to the root of you folder so pytest will extend sys.path() and will be able to discover all you modules withing the root directory.

You can also run pytest as python module. There is no need for any tricks or conftest.py files then. Just simply run

python -m pytest

You can point pytest to run certain folder with tests

pytest tests/unit

Or point it to run specific test module (a file).

pytest tests/unit/test_my_cool_funtionality.py

Or even a specific test function from a specific module.

pytest tests/unit/test_my_cool_funtionality.py::test_division

Of course all of this makes it super friendly to CI /CD tools.

Awesome test results output

Output is another strong feature of pytest. When tests pass output look green.

pytest all tests pass

However, when test(s) fails for every failed test pytest shows the failing assert, exact code snippet, all captured standard output and exception details. It makes it super easy to identify and fix a problem.

pytest, single test fails

Creating a test

The simplest way to create a test is to

  1. Create a file test_*.py
  2. Add a function def test_*()
  3. That is all 😊

Example:

import src.division as dv


def test_divide():
    assert 4 == dv.divide(8, 2)

To run use the command below.

python -m pytest tests/unit/test_simple_division.py
test_simple_division.py

Parameterized tests

Parameterization is a super important feature that allows to avoid lots of copy paste. Many other testing frameworks support parameterized tests, e.g. nUnit and xUnit in .NET. Luckily pytest has great support for parameterized tests too.

It is simple to create a parameterized test, here is how

import pytest

import src.division as dv


@pytest.mark.parametrize(
    "dividend, divisor, expected_result",
    [
        # (dividend, divisor, expected_result)
        (6, 3, 2), # <- Test case 1
        (-6, 3, -2), # <- Test case 2
        (6, -3, -2), # <- Test case 3
        (-6, -3, 2), # <- Test case 4
        (0, 3, 0), # <- Test case 5
    ])
def test_divide(dividend, divisor, expected_result):
    assert expected_result == dv.divide(dividend, divisor)

To run the test, use command below.

python -m pytest tests/unit/test_simple_division_parameterized.py
test_simple_division_parameterized.py

Test for Exceptions

Very often we need to test that our code throws appropriate exception when some invariant is not met. We can definitely implement that with try-catch, but there is much more elegant and simpler pytest solution. You can take advantage of pytest.raises(exception_here) in conjunction with context manager.

Example:

import pytest

import src.division as dv


def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        dv.divide(2, 0)

To run the test, use command below.

python -m pytest tests/unit/test_simple_division_exceptions.py

Fixtures

Fixtures are jewels of pytest. I haven’t seen similar implementations in any other framework. Of course, there is support for tests setup and tear down methods in other frameworks, but nothing like pytest fixtures.

What is pytest fixture

Pytest Fixture is a function, it contains code that can be run before and after the test. Fixture may or may not return a value. Pytest fixtures have different scope, e.g. session, module, class, function or recently introduced dynamic. Fixture can be auto-applied to all tests based on scope. Fixture may read test function context. Fixture can be shared using conftest.py file, so there is no need to import fixtures as pytest will auto-discover them. There are so much more things fixtures can do, but we want to be concise, so let’s focus on the most useful and often used features.

If you’d like to know more details, here is pytest fixtures documentation.

Simple Fixture

To create a fixture we just need to write a function and apply @pytest.fixture decorator to it.

import pytest


@pytest.fixture
def simple_fixture():
    print("SETUP : simple_fixture")
    yield
    print("TEARDOWN : simple_fixture")

To use fixture with a test function we have two options.

  1. Use decorator @pytest.mark.usefixtures(fixture1, fixture2, …).
  2. Pass fixture as test function argument. This works with parameterized tests too!
import pytest


@pytest.mark.usefixtures("simple_fixture")
def test_fixture_with_usefixtures_attribute():
    print(f"RUNNING : test {__name__}")
    # Make test fail to see the output
    assert False


def test_fixture_as_function_parameter(simple_fixture):
    print(f"RUNNING : test {__name__}")
    # Make test fail to see the output
    assert True

Fixture work well with parameterized tests too.

import pytest


@pytest.mark.parametrize(
    "param_1, param_2",
    [
        (1, 2),
        (3, 4)
    ])
def test_fixture_as_function_parameter_with_parameterized_tests(simple_fixture, param_1, param_2):
    print(f"RUNNING : test {__name__} with parameters {param_1}, {param_2}")
    # Make test fail to see the output
    assert True

To run the test, use command below.

python -m pytest -s -v tests/unit/test_fixture_simple.py
test_fixture_simple.py

Fixture returning value

Fixtures returning value are very handy when we need to obtain a resource before test runs and release it when test ends. A fixture can create and initialize a resource on setup and release it on tear down A resource can be a database connection, a temporary directory or file that needs to be created and then deleted.

Here is an example of DbConnection being opened and closed by a fixture.

import pytest


class DbConnection():
    def __init__(self):
        self.is_open = True

    def open(self):
        self.is_open = True
        print("DB connection is open")

    def execute_query(self, query):
        if not self.is_open:
            raise Exception("Connection is not open")
        print(f"Executing DB query: {query}")

    def close(self):
        self.is_open = False
        print("DB connection is closed")


@pytest.fixture
def db_connection():
    db_con = DbConnection()
    db_con.open()
    yield db_con
    db_con.close()


def test_fixture_returns_value(db_connection):
    db_connection.execute_query("SELECT * FROM dbo.Cats")
    # Make test fail to see the output
    assert True

To run the test, use command below.

python -m pytest -s -v tests/unit/test_fixture_return_value.py
test_fixture_return_value.py

Fixtures Scope

Fixture scopes determine when and how many times a fixture will run. If we apply multiple fixtures, the scope will determine the order at which fixtures are run. Higher scope of a fixture the earlier it will run setup code earlier and the later it will run tear down code.

  • function scope – runs every time a test function is run (default).
  • class scope – runs once per test class, regardless how may test methods the class has.
  • module scope – runs once per test module.
  • session scope – runs once per test session. Test session starts when you run pytest and ends when pytest exists.
  • dynamic scope.

Here is an example of session, module and function scoped fixtures applied to a single test function. The example also illustrates the order fixtures code run.

import pytest


@pytest.fixture(scope="session")
def session_fixture():
    print("session_fixture setup")
    yield
    print("session_fixture teardown")


@pytest.fixture(scope="module")
def module_fixture():
    print("module_fixture setup")
    yield
    print("module_fixture teardown")


@pytest.fixture
def function_fixture():
    print("function_fixture setup")
    yield
    print("function_fixture teardown")


def test_fixture_scope(module_fixture, session_fixture, function_fixture):
    print(f"RUNNING : test {__name__}")
    assert True

To run the test, use command below.

python -m pytest -v -s tests/unit/test_fixture_scope.py
test_fixture_scope.py

Auto Applied Fixture

Fixtures can automatically run without being explicitly applied or passed as an argument to a test function. We just need to pass autouse=True to a fixture decorator. The order at which auto fixture code gets executed is determined by the fixture scope. Auto fixtures are executed before non-auto fixtures withing the same scope.

Here is a example from the above, except that all fixtures are autouse and we do not explicitly pass them as test function parameters.

import pytest


@pytest.fixture(scope="session", autouse=True)
def session_fixture():
    print("session_fixture setup")
    yield
    print("session_fixture teardown")


@pytest.fixture(scope="module", autouse=True)
def module_fixture():
    print("module_fixture setup")
    yield
    print("module_fixture teardown")


@pytest.fixture(autouse=True)
def function_fixture():
    print("function_fixture setup")
    yield
    print("function_fixture teardown")


def test_fixture_scope():
    print(f"RUNNING : test {__name__}")
    assert True

To run the test, use command below.

python -m pytest -v -s tests/unit/test_fixture_autouse.py
test_fixture_autouse.py

conftest.py and auto-fixtures

It is really worth to mention that if auto-fixtures are placed in a conftest.py file they will auto-apply to all test functions and classes within the same and below levels of directory structure. That is very powerful way to ensure that fixtures are applied without the need of importing.

Reading test context in a fixture

There are a lot of built in fixtures in pytest, one of which is request, it helps to capture current test function context and make it available to a fixture.

Code below shows an example of how we can read module variable from a fixture.

import pytest

MODULE_VARIABLE = "MODULE_VARIABLE value"


@pytest.fixture
def awesome_fixture(request):
    print("function_fixture setup, running from")
    print(f"Module: '{request.module.__name__}''")
    print(f"Function: '{request.function.__name__}''")
    print(f"Module variable: {request.module.MODULE_VARIABLE}")

    print("SETUP")
    yield
    print("TEARDOWN")


def test_fixture_context(awesome_fixture):
    print(f"RUNNING : {__name__}")
    assert True

To run the test, use command below.

python -m pytest -v -s tests/unit/test_fixture_capture_context.py
test_fixture_capture_context.py

Summary

I hope you find this short tutorial useful. We covered the most useful pytest features that will make you productive every day. If you think I missed something or you have a pytest productivity trick, please share in the comments below!

Posts created 20

2 thoughts on “pytest in 5 minutes

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top