How to Write Custom Pytest Plugins for Advanced Test Functionality

Pytest is one of Python’s most extensible testing frameworks, allowing developers to create custom plugins to extend its functionality. Whether you need to add new command-line options, implement custom fixtures, or automate repetitive tasks, Pytest plugins enable you to tailor the framework to your unique requirements.

In this article, we’ll explore how to write a custom Pytest plugin, register it, and integrate it into your test workflows.

What Are Pytest Plugins?

Pytest plugins are Python modules or packages that enhance the core functionality of Pytest. They allow developers to:

Plugins can be local (specific to a project) or distributed via PyPI for others to use.

How to Create a Simple Pytest Plugin

Let’s start by creating a simple plugin that adds a custom marker to track slow tests.

Step 1: Define the Plugin

Create a file named conftest.py in your project. This file acts as the entry point for Pytest to discover and load custom plugins.

import pytest

# Custom marker for slow tests
def pytest_configure(config):  
    config.addinivalue_line(  
        "markers", "slow: mark test as slow"  
    )

# Hook to skip slow tests if --skip-slow option is used
def pytest_addoption(parser):  
    parser.addoption(  
        "--skip-slow", action="store_true", default=False, help="Skip tests marked as slow"  
    )

def pytest_collection_modifyitems(config, items):  
    if config.getoption("--skip-slow"):  
        skip_slow = pytest.mark.skip(reason="Skipped slow tests")  
        for item in items:  
            if "slow" in item.keywords:  
                item.add_marker(skip_slow)

Explanation

  1. pytest_configure: Registers a new marker (slow) with Pytest.
  2. pytest_addoption: Adds a custom command-line option (--skip-slow) to control plugin behavior.
  3. pytest_collection_modifyitems: Modifies test collection to skip tests marked with slow if the --skip-slow option is used.

Step 2: Mark Tests

Use the custom marker @pytest.mark.slow in your test cases:

import pytest

@pytest.mark.slow
def test_slow_function():  
    assert sum(range(10**6)) > 0

def test_fast_function():  
    assert 1 + 1 == 2

Step 3: Run Tests

Run tests normally:

pytest

Skip slow tests with the --skip-slow option:

pytest --skip-slow

This basic plugin showcases how to define custom markers and command-line options.

How to Write Reusable Plugins

To distribute your plugin for reuse, package it as a Python module and publish it to PyPI.

Step 1: Create the Package Structure

Organize your plugin as a Python package:

pytest_slow/  
├── pytest_slow/  
│   ├── __init__.py  
│   ├── plugin.py  
├── setup.py  
├── README.md  
└── LICENSE

Step 2: Define the Plugin

Move the plugin code to plugin.py inside the pytest_slow directory.

import pytest

def pytest_configure(config):  
    config.addinivalue_line(  
        "markers", "slow: mark test as slow"  
    )

def pytest_addoption(parser):  
    parser.addoption(  
        "--skip-slow", action="store_true", default=False, help="Skip tests marked as slow"  
    )

def pytest_collection_modifyitems(config, items):  
    if config.getoption("--skip-slow"):  
        skip_slow = pytest.mark.skip(reason="Skipped slow tests")  
        for item in items:  
            if "slow" in item.keywords:  
                item.add_marker(skip_slow)

Step 3: Add Setup Configuration

Create a setup.py file to define the package:

from setuptools import setup

setup(  
    name="pytest-slow",  
    version="0.1.0",  
    py_modules=["pytest_slow.plugin"],  
    install_requires=["pytest"],  
    entry_points={  
        "pytest11": [  
            "pytest_slow = pytest_slow.plugin",  
        ],  
    },  
    description="A pytest plugin to skip slow tests",  
    long_description=open("README.md").read(),  
    classifiers=["Framework :: Pytest"],  
)

Step 4: Publish the Plugin

How to Debug and Test Plugins

Test plugins thoroughly using the Pytest hooks provided in the pytester plugin:

def test_skip_slow(pytester):  
    pytester.makepyfile(  
        """  
        import pytest  
        @pytest.mark.slow  
        def test_slow():  
            assert True  
        def test_fast():  
            assert True  
        """  
    )

    result = pytester.runpytest("--skip-slow")  
    result.assert_outcomes(passed=1, skipped=1)

This ensures your plugin behaves as expected.

Conclusion

Custom Pytest plugins unlock the potential to streamline and enhance your testing workflows. By defining plugins for tasks like skipping tests, managing environments, or generating reports, you can improve your test suite’s efficiency and maintainability.

Start experimenting with Pytest plugins today and create powerful, reusable tools tailored to your projects!