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:
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:
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:
Show all recorded output
For complex diffs it helps to see the full recorded output also. To enable this use:
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 (tempfilemodule,tmpdirfixture, ...) - replacing hexadecimal values
0x...of arbitrary length with0x?????????.
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.