Testing Python applications is critical to ensuring they function as intended in real-world scenarios. Among the numerous testing frameworks available for Python, pytest stands out as a primary choice for many developers. Renowned for its straightforward yet powerful features, pytest makes writing and running tests efficient and effective.

In this guide, we’ll explore how to get started with pytest, from installation to writing your first tests. We’ll also discuss how automating pytest within a continuous integration (CI) environment can simplify your testing process, making it more consistent, reliable, and efficient. By integrating pytest with CI, you can catch bugs early and often, enhancing the overall quality of your software products.

What is pytest?

Pytest is a popular, flexible, and easy-to-use software testing framework for Python. It offers a comprehensive suite of features to streamline the testing process, making it ideal for both simple unit tests and complex functional testing.

With pytest, you can write concise and readable test cases using Python’s familiar syntax. Additionally, pytest’s ecosystem of plugins boosts its capabilities and allows you to create customized testing workflows.

Python’s simple syntax and readability make writing test cases easy. This allows you to create comprehensive test suites to ensure the quality and reliability of your software projects.

Key features of pytest

Pytest’s unique features make it one of the most popular testing tools for Python code. These features include:

  • Simple syntax: Pytest offers an easy approach to writing tests, making it accessible to beginners.
  • Assertion introspection: Unique to pytest, assertion introspection provides easier debugging by highlighting the exact point of failure in a test.
  • Compatibility: Pytest seamlessly integrates with existing unit test and nose test suites, allowing you to migrate to pytest without rewriting tests.
  • Plugin-based architecture: The framework’s extensibility through plugins supports the customization and enhancement of its core functionalities.

While pytest is particularly adept at handling unit tests, you can use its plugins and fixtures to handle more complex test scenarios at higher levels of the testing pyramid for a more comprehensive testing strategy.

Benefits of pytest

Let’s review some additional benefits of using pytest to automate Python testing:

  • Reduced boilerplate code: Pytest reduces boilerplate code, allowing you to write concise and expressive tests. This enhances test readability and maintainability, leading to more efficient debugging and code comprehension.
  • Extensive fixture support: Pytest also provides extensive support for fixtures, simplifying setup and teardown tasks and promoting code reuse. This promotes flexibility and scalability in test automation.
  • Active community — pytest has an active community that continuously develops plugins that extend its functionality, catering to diverse testing needs.

Installing pytest

The following sections will review the hands-on steps to start using pytest for automated Python testing. Before getting started, ensure you have Python installed on your local machine. Note that this also includes pip, which you will use for pytest installation.

To install pytest, open your terminal or command prompt. Then, run pip install pytest, which will download and install pytest and its dependencies.

Next, verify that the installation was successful by running the following command. This displays the installed version of pytest, confirming you have successfully installed it.

pytest --version

Now, run a basic text to ensure pytest is functioning properly. Create a simple test file named test_example.py with the following code. This checks if a simple arithmetic operation (addition) is correct:

def test_answer():
    assert 5 == 2 + 3

To run this test, navigate to the directory containing test_example.py in your terminal or command prompt and execute the following command:

pytest

pytest will discover and run the test, then report the results. If the test passes, it indicates that you have successfully installed pytest and it is ready for use.

Pytest fixtures

In pytest, fixtures help you set up and tear down the test environment or state before and after tests run. Fixtures help to ensure that tests run under consistent conditions, making test outcomes reliable and repeatable.

Fixtures promote modularity by allowing you to abstract test setup and teardown logic into reusable components. This abstraction means that you can use the same setup across multiple tests, reducing code duplication and fostering a cleaner, more maintainable test suite.

Let’s review how to test a simple caching system using fixtures to manage setup and teardown automatically.

To get started with pytest fixtures, create a file named test_example.py with this content:

# test_example.py
import pytest

class SimpleCache:
    def __init__(self):
        self.store = {}

    def set(self, key, value):
        self.store[key] = value

    def get(self, key):
        return self.store.get(key, None)

@pytest.fixture
def cache():
    """Fixture to provide a fresh SimpleCache instance to each test."""
    test_cache = SimpleCache()
    yield test_cache  # Test runs with this instance of SimpleCache.
    # Teardown can be more complex if needed, but here we just clear the cache.
    test_cache.store.clear()

def test_cache_set_and_get(cache):
    cache.set("test_key", "test_value")
    assert cache.get("test_key") == "test_value", "Value should be retrievable after being set."

def test_cache_miss_returns_none(cache):
    assert cache.get("nonexistent_key") is None, "Cache miss should return None."

This example introduces a SimpleCache class that represents a simple key-value cache implementation and uses a pytest fixture to provide a fresh instance of this cache to each test. Consequently, tests for setting and retrieving values and handling cache misses run with a clean state.

This approach enhances test modularity by centralizing the cache initialization and cleanup within the fixture, ensuring that cache-related tests are easily maintainable and extensible. It also promotes reusability by allowing different tests to utilize the same setup.

To verify that your caching system works as expected, navigate to the directory containing test_example.py from your terminal or command prompt. Then, run the tests using the following command:

pytest test_example.py

Writing tests in pytest

Thanks to its minimalistic syntax and powerful features, writing tests in pytest is straightforward. That said, there are some guidelines you should follow to write basic tests effectively.

To begin writing basic tests in pytest, create a Python file with names prefixed with test to indicate they contain tests. Within each test function, use names that clearly describe what you are testing — for example, test_addition() or test_file_reading().

To assert outcomes, use built-in assert statements or pytest’s assert helper functions like assertEqual() or assertTrue(). Ensure each test function raises an AssertionError if the condition fails, providing clear feedback on the test outcome.

Organize tests logically, focusing on one aspect per test function, and run them using pytest’s command-line tool or integrated development environment (IDE) integration for streamlined testing.

Let’s walk through two examples of how to write tests in pytest.

Testing a sum function

Suppose you have a simple function, sum_two_numbers, that you want to test:

# sum.py
def sum_two_numbers(a, b):
    return a + b

To write a pytest test for this function, use the code below:

# test_sum.py
from sum import sum_two_numbers
def test_sum_two_numbers():
    assert sum_two_numbers(1, 2) == 3, "Expected sum of 1 and 2 to be 3"

This test checks that sum_two_numbers correctly calculates the sum of 1 and 2. The assert statement verifies the function’s output against the expected result.

Testing a swap function

As another example, say you want to test a function that swaps the values of two variables using a temporary variable:

# swap.py
def swap_values(a, b):
    tmp = a
    a = b
    b = tmp
    return a, b

The pytest test to check this function is:

# test_swap.py
from swap import swap_values
def test_swap_values():
    assert swap_values(3, 4) == (4, 3), "Values should be swapped"

Parameterizing pytest

Parameterized tests in pytest allow you to run a single test function multiple times with different input arguments. Through parameterization, each set of arguments effectively becomes a separate test case, facilitating comprehensive testing with minimal code. Using decorators like @pytest.mark.parametrize, you can specify parameters and corresponding values for each test iteration.

For instance, @pytest.mark.parametrize('input, expected', [(1, 2), (2, 4), (3, 6)]) defines input-output pairs. During test execution, pytest will run the test function for each parameter set, reporting individual outcomes.

Parameterized tests streamline testing by consolidating similar tests into a single function, promoting code readability and maintainability while ensuring thorough test coverage.

Returning to the swap function from the previous section, use the code below to write a parameterized test:

# test_swap_parametrized.py
import pytest
from swap import swap_values

@pytest.mark.parametrize("input_a, input_b, expected_a, expected_b", [
	(1, 2, 2, 1),       # Test with integers
	('a', 'b', 'b', 'a'), # Test with strings
	(True, False, False, True), # Test with boolean values
])
def test_swap_values(input_a, input_b, expected_a, expected_b):
    result_a, result_b = swap_values(input_a, input_b)
    assert result_a == expected_a and result_b == expected_b

The @pytest.mark.parametrize decorator specifies parameters (input_a, input_b, expected_a, expected_b) and their values for each test case, with each tuple representing a unique set of arguments. Pytest then executes the test_swap_values function with these tuples, creating multiple test cases from a single function to cover various input scenarios.

Pytest plugins

You can significantly extend pytest’s functionality by using plugins. Plugins allow you to customize your testing environment to meet the specific needs of your projects. These plugins can add new fixtures, hooks, command-line options, and even integrate with other frameworks.

Some popular pytest plugins:

  • pytest-django — Tailored for Django applications, pytest-django simplifies testing by integrating pytest with Django’s test framework. Doing so supports the efficient testing of Django models, views, and templates.
  • pytest-asyncio — pytest-asyncio helps test asyncio coroutines and provides support for asynchronous programming in Python.
  • pytest-cov — This plugin offers comprehensive coverage reporting for pytest tests, integrating with the coverage.py tool. It also helps identify untested code segments.
  • pytest-mock — This plugin enhances pytest with support for the mock library, facilitating the mocking of classes, objects, and functions in tests.

Continuous integration with pytest

Testing your Python apps with pytest is an important step toward ensuring your software operates correctly and efficiently. But running tests manually after every change can be cumbersome and error-prone. You may overlook critical test cases, test in inconsistent environments, or forget to test your changes altogether, all of which can jeopardize the stability of your application.

CI ensures that every piece of code is tested automatically before it is integrated into the main branch. This continuous testing loop makes it faster and easier for you to detect and resolve bugs. It also makes collaboration more efficient, providing team-wide visibility into the health of your codebase.

CircleCI provides first-class support for Python developers and makes automating your pytest jobs straightforward and efficient. This support includes dependency caching to speed up build times, easy configuration for parallel test execution, and the ability to quickly set up different Python environments to ensure compatibility across multiple versions.

To get started running your Python tests in a CI pipeline in 15 minutes or less, follow our Python documentation. For more specific applications, check out the following resources that guide you through setting up and optimizing your pytest configurations:

  • Testing a Flask framework with pytest — This resource provides detailed instructions on setting up pytest within a CI pipeline for Flask applications, ensuring that automated CI processes thoroughly test web applications built with Flask.

  • Testing a PyTorch machine learning model with pytest — Designed for developers working with PyTorch machine learning (ML) models, this guide explains how to incorporate pytest into the testing workflow. It demonstrates how to automate the testing of ML code to verify model behavior and performance.

By using pytest in your CI pipelines, you can automate your testing process, improve software quality, and streamline the development workflow — all things that make it easier to maintain and evolve complex applications.

Conclusion

This article explored the ways pytest can automate testing your Python applications. From installation to advanced features like fixtures and plugins, you now have the tools and knowledge to implement effective automated testing strategies.

For developers seeking to strengthen their testing methodologies, embracing pytest could be transformative. Not only does it simplify test creation and execution, but it also supports practices like continuous integration and test-driven development. Sign up for your free CircleCI account and get started building more robust and reliable Python applications today.

Start Building for Free