Metadata-Version: 2.4
Name: pytest-multi-backend
Version: 2026.9.7
Summary: Run one pytest suite against several interchangeable backends.
Author-email: Adam Dangoor <adamdangoor@gmail.com>
License-Expression: MIT
Project-URL: Documentation, https://adamtheturtle.github.io/pytest-multi-backend/
Project-URL: Source, https://github.com/adamtheturtle/pytest-multi-backend
Keywords: backend,fake,mock,pytest,testing
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.12
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: beartype>=0.22.9
Requires-Dist: pytest>=8.4.0
Provides-Extra: dev
Requires-Dist: actionlint-py==1.7.12.24; extra == "dev"
Requires-Dist: check-manifest==0.51; extra == "dev"
Requires-Dist: check-wheel-contents==0.6.3; extra == "dev"
Requires-Dist: coverage==7.16.0; extra == "dev"
Requires-Dist: deptry==0.25.1; extra == "dev"
Requires-Dist: doc8==2.0.0; extra == "dev"
Requires-Dist: doccmd==2026.9.1; extra == "dev"
Requires-Dist: furo==2025.12.19; extra == "dev"
Requires-Dist: interrogate==1.7.0; extra == "dev"
Requires-Dist: mypy[faster-cache]==2.3.1; extra == "dev"
Requires-Dist: mypy-strict-kwargs==2026.8.25.1; extra == "dev"
Requires-Dist: no-defaults==2026.9.1; extra == "dev"
Requires-Dist: prek==0.5.2; extra == "dev"
Requires-Dist: pydocstringformatter==1.0.0; extra == "dev"
Requires-Dist: pydocstyle==6.3; extra == "dev"
Requires-Dist: pylint[spelling]==4.0.8; extra == "dev"
Requires-Dist: pylint-per-file-ignores==3.2.1; extra == "dev"
Requires-Dist: pyproject-fmt==2.29.3; extra == "dev"
Requires-Dist: pyrefly==1.2.0; extra == "dev"
Requires-Dist: pyright==1.1.411; extra == "dev"
Requires-Dist: pyroma==5.0.1; extra == "dev"
Requires-Dist: pytest==9.1.1; extra == "dev"
Requires-Dist: pytest-beartype-tests==2026.8.16; extra == "dev"
Requires-Dist: ruff==0.16.6; extra == "dev"
Requires-Dist: shellcheck-py==0.11.0.1; extra == "dev"
Requires-Dist: shfmt-py==4.1.0; extra == "dev"
Requires-Dist: sphinx==9.1.0; extra == "dev"
Requires-Dist: sphinx-copybutton==0.5.2; extra == "dev"
Requires-Dist: sphinx-lint==1.0.2; extra == "dev"
Requires-Dist: sphinx-paramlinks==0.6; extra == "dev"
Requires-Dist: sphinx-pyproject==0.3.0; extra == "dev"
Requires-Dist: sphinx-substitution-extensions==2026.8.13.1; extra == "dev"
Requires-Dist: sphinxcontrib-spelling==8.0.2; extra == "dev"
Requires-Dist: sphinxcontrib-towncrier==0.5.0a0; extra == "dev"
Requires-Dist: strict-kwargs==2026.8.28.post2; extra == "dev"
Requires-Dist: sybil==10.1.0; extra == "dev"
Requires-Dist: towncrier==25.8.0; extra == "dev"
Requires-Dist: ty==0.0.78; extra == "dev"
Requires-Dist: vale==3.20.0.0; extra == "dev"
Requires-Dist: vulture==2.16; extra == "dev"
Requires-Dist: yamlfix==1.19.1; extra == "dev"
Requires-Dist: zizmor==1.30.0; extra == "dev"
Provides-Extra: release
Requires-Dist: check-wheel-contents==0.6.3; extra == "release"
Requires-Dist: towncrier==25.8.0; extra == "release"
Dynamic: license-file

|Build Status| |PyPI|

pytest-multi-backend
====================

.. contents::
   :local:

Run one ``pytest`` suite against several interchangeable backends.

A "backend" is one way of running the system which the tests exercise.
A suite might run against a real remote service, an in-memory fake of that service, and the same fake behind an HTTP server, and assert the same things about each.
That is how a fake is kept honest.

Installation
------------

.. code-block:: shell

    pip install pytest-multi-backend

This requires Python |minimum-python-version|\+.

Usage
-----

Backends are the members of an ``Enum``.
The member name, in lower case, is what ``--skip-backend`` takes to skip the backend.
The member value is the ID which ``pytest`` shows for it.

``backend_fixture`` makes a fixture which runs each test which uses it once per backend.
Assign the fixture to a module-level name in a ``conftest.py`` for ``pytest`` to find it.

.. code-block:: python

    """Run each test against the real service and against a fake."""

    from collections.abc import Generator
    from enum import Enum

    import pytest

    from pytest_multi_backend import backend_fixture


    class Backend(Enum):
        """The ways of running the service under test."""

        REAL = "Real service"
        FAKE = "In memory fake"


    def _setup_backend(
        *,
        backend: Backend,
        request: pytest.FixtureRequest,
    ) -> Generator[None]:
        """Set a backend up, yield while the test runs, then tear it down.

        Anything else the setup needs comes from
        ``request.getfixturevalue``.
        """
        monkeypatch = request.getfixturevalue(argname="monkeypatch")
        if backend is Backend.FAKE:
            # Start the fake here, and point the code under test at it.
            monkeypatch.setenv(name="SERVICE_URL", value="http://localhost:8080")
            yield
            # Stop the fake here.
            return
        monkeypatch.setenv(name="SERVICE_URL", value="https://example.com")
        yield


    fixture_backend = backend_fixture(
        name="backend",
        backends=list(Backend),
        setup_for=_setup_backend,
    )

A test which requests the fixture runs once per backend, and gets the backend it is running against:

.. code-block:: python

    """A test which runs against each backend."""

    from enum import Enum


    def test_something(backend: Enum) -> None:
        """Assert the same thing about every backend."""
        assert backend.value

Skipping backends
~~~~~~~~~~~~~~~~~

Give ``--skip-backend`` the lower case name of a backend to skip every test against it.
Give it once per backend to skip.

.. code-block:: console

   $ pytest --skip-backend=real

Tests against a skipped backend are skipped, not deselected, so a run which skips a backend still reports the tests which would have used it.
The setup function is not called for a skipped backend.
Giving a name which no fixture has a backend for is an error, so a mistyped name does not silently run tests against a backend you meant to avoid.

Skipping marked tests
~~~~~~~~~~~~~~~~~~~~~

Give ``--skip-marker`` the name of a registered marker to skip every test which carries it.
Give it once per marker to skip.

.. code-block:: console

   $ pytest --skip-marker=requires_docker

Unlike ``-m "not requires_docker"``, this skips rather than deselects, so the skipped tests are still reported.
The marker must be registered in the ``markers`` setting, as ``--strict-markers`` requires, and giving an unregistered marker is an error.

Full documentation
------------------

See the `full documentation <https://adamtheturtle.github.io/pytest-multi-backend/>`__.

.. |Build Status| image:: https://github.com/adamtheturtle/pytest-multi-backend/actions/workflows/test.yml/badge.svg?branch=main
   :target: https://github.com/adamtheturtle/pytest-multi-backend/actions
.. |PyPI| image:: https://badge.fury.io/py/pytest-multi-backend.svg
    :target: https://badge.fury.io/py/pytest-multi-backend
.. |minimum-python-version| replace:: 3.12
