Skip to content

Other Features

Using the regtest fixture as context manager

# test_squares.py

def test_squares(regtest):

    result = [i*i for i in range(10)]

    with regtest:
        print(result)

The regtest_all fixture

The regtest_all fixture leads to recording of all output to stdout in a test function. This is particularly useful for legacy code that prints extensively.

# test_squares.py

def test_all(regtest_all):
    print("this line will be recorded.")
    print("and this line also.")

Warning

Since regtest_all captures everything sent to stdout, it may also include non-deterministic output from libraries (e.g., timestamps in logs). Use custom converters to clean such output if necessary.

Reset individual tests

You can reset recorded output of files and functions individually as:

$ pytest --regtest-reset test_demo.py
$ pytest --regtest-reset test_demo.py::test_squares

Use semantic diff for Python object snapshots

For Python object snapshots, --regtest-ddiff uses the deepdiff library to show structural differences instead of a unified text diff:

$ pytest --regtest-ddiff ...

This produces output like {'iterable_item_removed': {'root[3]': 4}}, which is often easier to read for nested dicts and lists than a raw line diff.

The output format depends on color support: when the terminal supports color (or when --color=yes is passed to pytest), deepdiff switches to its colored view, which uses a different — and more readable — layout than the plain-text view.

Suppress diff for failed tests

To hide the diff and just show the number of lines changed, use:

$ py.test --regtest-nodiff ...

Show all recorded output

For complex diffs it helps to see the full recorded output also. To enable this use:

$ py.test --regtest-tee...

Line endings

Per default pytest-regtest ignores different line endings in the output. In case you want to disable this feature, use the -regtest-consider-line-endings flag.

Clean nondeterministic output before recording

Output can contain data which is changing from test run to test run, e.g. paths created with the tmpdir fixture, hexadecimal object ids or timestamps.

Per default the plugin helps to make output more deterministic by:

  • replacing all temporary folder in the output with <tmpdir...> or similar markers, depending on the origin of the temporary folder (tempfile module, tmpdir fixture, ...)
  • replacing hexadecimal values 0x... of arbitrary length with 0x?????????.

You can also implement your own cleanup routines as described below.

Register own cleanup functions

You can register own converters in conftest.py:

import re
import pytest_regtest

@pytest_regtest.register_converter_pre
def remove_password_lines(txt):
    '''modify recorded output BEFORE the default fixes
    like temp folders or hex object ids are applied'''

    # remove lines with passwords:
    lines = txt.splitlines(keepends=True)
    lines = [l for l in lines if "password is" not in l]
    return "".join(lines)

@pytest_regtest.register_converter_post
def fix_time_measurements(txt):
    '''modify recorded output AFTER the default fixes
    like temp folders or hex object ids are applied'''

    # fix time measurements:
    return re.sub(
        r"\d+(\.\d+)? seconds",
        "<SECONDS> seconds",
        txt
    )

If you register multiple converters they will be applied in the order of registration.

In case your routines replace, improve or conflict with the standard cleanup converters, you can use the flag --regtest-disable-stdconv to disable the default cleanup procedure.

Command line options summary

These are all supported command line options:

$ pytest --help | grep regtest
  --regtest-reset       do not run regtest but record current output
  --regtest-tee         print recorded results to console too
  --regtest-consider-line-endings
  --regtest-nodiff      do not show diff output for failed regression tests
  --regtest-ddiff       use deltadiff library to show snapshot diffs
  --regtest-disable-stdconv

Implementing your own snapshot handlers

For reference, the following code shows the implementation of the built-in PythonObjectHandler.

class PythonObjectHandler(BaseSnapshotHandler):
    def __init__(self, handler_options, pytest_config, tw):
        self.compact = handler_options.get("compact", False)
        self.format_ = handler_options.get("format", "pickle")
        if self.format_ not in ("pickle", "json"):
            raise ValueError(f"unknown format = {self.format_!r}")

    def save(self, folder, obj):
        for name in ["object.pkl", "object.json"]:
            path = os.path.join(folder, name)
            if os.path.exists(path):
                os.unlink(path)

        if self.format_ == "pickle":
            with open(os.path.join(folder, "object.pkl"), "wb") as fh:
                pickle.dump(obj, fh)
        elif self.format_ == "json":
            try:
                with open(os.path.join(folder, "object.json"), "w") as fh:
                    json.dump(obj, fh)
            except TypeError as e:
                raise TypeError(
                    f"{e}. Only JSON-serializable types (dict, list, str, int, "
                    f"float, bool, None) are supported with format='json'. "
                    f"Use format='pickle' for arbitrary Python objects."
                ) from e

    def load(self, folder):
        try:
            with open(os.path.join(folder, "object.pkl"), "rb") as fh:
                return pickle.load(fh)
        except FileNotFoundError:
            with open(os.path.join(folder, "object.json"), "r") as fh:
                return json.load(fh)

    def show(self, obj):
        return pprint.pformat(obj, compact=self.compact).splitlines()

    def compare(self, current_obj, recorded_obj):
        return recorded_obj == current_obj

    def show_differences(self, current_obj, recorded_obj, has_markup, use_ddiff):
        if not use_ddiff:
            return list(
                difflib.unified_diff(
                    self.show(current_obj),
                    self.show(recorded_obj),
                    "current",
                    "expected",
                    lineterm="",
                )
            )
        ddiff = DeepDiff(
            current_obj, recorded_obj, view=COLORED_VIEW if has_markup else "text"
        )
        from io import StringIO

        fh = StringIO()
        print(ddiff, file=fh)
        return fh.getvalue().splitlines()
def register_python_object_handler():
    SnapshotHandlerRegistry.add_handler(
        lambda obj: isinstance(obj, (int, float, str, list, tuple, dict, set)),
        PythonObjectHandler,
    )

Example

If you have a custom class, you can register a handler for it in conftest.py:

import os
import json
import pytest_regtest
from pytest_regtest.snapshot_handler import (
    BaseSnapshotHandler,
    SnapshotHandlerRegistry,
)

class MyData:
    def __init__(self, value):
        self.value = value

class MyDataHandler(BaseSnapshotHandler):
    def __init__(self, options, config, tw):
        pass

    def save(self, folder, obj):
        # Use a stable serialization format
        with open(os.path.join(folder, "data.json"), "w") as f:
            json.dump(obj.value, f)

    def load(self, folder):
        with open(os.path.join(folder, "data.json"), "r") as f:
            value = json.load(f)
            return MyData(value)

    def show(self, obj):
        return [f"MyData(value={obj.value})"]

    def compare(self, current, recorded):
        return current.value == recorded.value

    def show_differences(self, current, recorded, has_markup):
        return [f"Actual: {current.value}",
                f"Expected: {recorded.value}"]

# Register the handler: the first argument is a check function
# which returns True if the handler should be used for the given object.
SnapshotHandlerRegistry.add_handler(
    lambda obj: isinstance(obj, MyData),
    MyDataHandler
)

Note

When implementing save and load, ensure you use a stable serialization format (like JSON or pickle). Avoid simple string conversions unless you are certain the data can be perfectly reconstructed.