Python in the Enterprise Part 3: Unit testing with pytest

The unittest module, in the Python standard library, is based on JUnit (a Java library) and thus doesn’t feel very Pythonic. Pytest turns this upside down and provides a more Python developer friendly experience for writing unit tests.

Installation

$ pip install pytest

Basic test

Unlike the unittest module which defines multiple assertXXX methods for writing assertions, pytest simply uses Python’s built-in assert statement.

# tests/test_hello.py
def test():
    assert "abc" == "def"

Running from command line

$ pytest -v tests/

Pytest is still able to show detailed information about why a test case failed even though it uses the built in assert statements:

_____________________________ test_abc _____________________________

    def test_abc():
>       assert "abc" == "def"
E       AssertionError: assert 'abc' == 'def'
E         - def
E         + abc

tests/test_hello.py:3: AssertionError
===================== short test summary info ======================
FAILED tests/test_hello.py::test_abc - AssertionError: assert 'ab...

Asserting that exceptions are raised

import pytest

class GreetingException(Exception):
    def __init__(self, name):
        self.name = name

def test_ex():
    with pytest.raises(GreetingException) as exc_info:
        raise(GreetingException("bob"))
    # Exception is captured in exc_info.value 
    assert exc_info.value.name == "bob"

Fixtures and arguments

import pytest
from unittest.mock import Mock

@pytest.fixture
def greeting_service():
    return Mock()

# test case that uses the greeting_service fixture
def test_greeting(greeting_service):
    greeting_service.greet()

Sharing fixtures among test modules

Move the greeting_service fixture to a file name conftest.py:

# conftest.py
import pytest
from unittest.mock import Mock

@pytest.fixture
def greeting_service():
    return Mock()
# test_greeting2.py
def test_greeting2(greeting_service):
    greeting_service.greet()

The fixture function will be invoked for each test method that depends on it - this is called “function” scope. If setting up the fixture is expensive a different scope can be assigned to a text fixture. The supported scopes are - function (the default), class, module, package or session.

import pytest

@pytest.fixture(scope="module")
def greeting_service():
    pass

module scope here means that all the test cases in a Python module will share the same instance of the fixture.

Cleaning up fixtures

import pytest
from unittest.mock import Mock

@pytest.fixture
def greeting_service():
    service = Service()
    yield service
    # Clean up code for service
    service.close()

Disabling stdout capture

By default, pytest will suppress any messages that test cases print to stdout/stderr. To disable this behavior use the -s option.

$ pytest -s -v tests/

async test functions

To test async functions, install pytest-asyncio:

$ pip install pytest-asyncio

Then decorate your test functions with @pytest.mark.asyncio:

import pytest
import asyncio

@pytest.mark.asyncio
async def test_async():
    await asyncio.sleep(5)

Running a test case multiple times

Sometimes a piece of logic works if invoked once but fails if invoked multiple times. For this, we can instruct pytest to run a test case multiple times using the pytest-repeat package.

$ pip install pytest-repeat

With the command line we can use the --count argument to execute the tests multiple times:

$ pytest --count=50 -v tests/test_greeting.py

In code, we can use the decorator @pytest.mark.repeat to specify the number of times to repeat a test case:

import pytest

@pytest.mark.repeat(5)
def test_greeting():
    pass

Comments

comments powered by Disqus