An open API service indexing awesome lists of open source software.

https://github.com/metahris/pytest-verify

a snapshot testing plugin for pytest that ensures your test outputs remain consistent across runs.
https://github.com/metahris/pytest-verify

compare dataclasses dataframe json numpy-arrays plugin pydantic-v2 pytest snapshot-testing text xml yaml

Last synced: 5 months ago
JSON representation

a snapshot testing plugin for pytest that ensures your test outputs remain consistent across runs.

Awesome Lists containing this project

README

          

# pytest-verify
[![PyPI version](https://badge.fury.io/py/pytest-verify.svg)](https://pypi.org/project/pytest-verify/)
[![Python Versions](https://img.shields.io/pypi/pyversions/pytest-verify.svg)](https://pypi.org/project/pytest-verify/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Downloads](https://static.pepy.tech/badge/pytest-verify)](https://pepy.tech/project/pytest-verify)
[![Build Status](https://github.com/metahris/pytest-verify/actions/workflows/run_tests.yml/badge.svg)](https://github.com/metahris/pytest-verify/actions)

**pytest-verify** is a snapshot testing plugin for **pytest** that
ensures your test outputs remain consistent across runs.

It automatically saves and compares snapshots of your test results and
can launch a **visual diff viewer** for reviewing differences
directly in your terminal.

---

## Installation

Basic installation:

pip install pytest-verify

---

## Usage Overview

Any pytest test that **returns a value** can be decorated with
`@verify_snapshot`.

- On the **first run**, pytest-verify creates baseline snapshots.
- On **subsequent runs**, it compares the new output with the expected
snapshot.
- If differences are detected, a diff is displayed:

![screenshot](docs/images/test_simple_json_failed.png)

🗨⚠️ Test FAILED, pytest-verify will ask whether to replace the expected.

💡 How This Works ?
===================

``` python
from pytest_verify import verify_snapshot

@verify_snapshot(
ignore_fields=[
"$.user.profile.updated_at",
"$.devices[*].debug",
"$.sessions[*].events[*].meta.trace",
],
abs_tol=0.05,
rel_tol=0.02,
)
def test_ignore_multiple_fields():
"""
- Exact path: $.user.profile.updated_at
- Array wildcard: $.devices[*].debug
- Deep nested wildcard: $.sessions[*].events[*].meta.trace
"""
return {
"user": {
"id": 7,
"profile": {"updated_at": "2025-10-14T10:00:00Z", "age": 30},
},
"devices": [
{"id": "d1", "debug": "alpha", "temp": 20.0},
{"id": "d2", "debug": "beta", "temp": 20.1},
],
"sessions": [
{"events": [{"meta": {"trace": "abc"}, "value": 10.0}]},
{"events": [{"meta": {"trace": "def"}, "value": 10.5}]},
],
}
```

The decorator ``@verify_snapshot`` automatically performs the following steps:

1. **Format Detection**
Detects that the return value is a JSON snapshot (because the test returns a Python ``dict``).

2. **Serialization**
Serializes the result into a canonical, pretty-formatted JSON string.

3. **Comparison**
Compares the serialized result against the existing ``.expected.json`` snapshot file.

5. **Baseline Creation**
On the first test run, if no snapshot exists, a baseline file is created:

::

__snapshots__/test_ignore_fields_complex.expected.json

6. **Subsequent Runs**
On later runs, the result is compared to the existing snapshot.
If differences occur outside the ignored fields or tolerance limits,
a unified diff is displayed in the terminal.

## Examples

``` python
from pytest_verify import verify_snapshot

@verify_snapshot()
def test_text_snapshot():
return "Hello, pytest-verify!"
```

**Passes when:** - The returned text is identical to the saved
snapshot. - Whitespace at the start or end of the string is ignored.

**Fails when:** - The text content changes (e.g. `"Hello, pytest!"`).

---

``` python
from pytest_verify import verify_snapshot
# ignore fields
@verify_snapshot(ignore_fields=["id"])
def test_ignore_fields():
return {"id": 2, "name": "Mohamed"}
```

**Passes when:** - Ignored fields differ (`id`), but all other keys
match.

**Fails when:** - Non-ignored fields differ (e.g. `"name"`).

---

``` python
# numeric tolerance
@verify_snapshot(abs_tol=1e-3; rel_tol=1e-3)
def test_with_tolerance():
return {"value": 3.1416}
```

**Passes when:** - Numeric values differ slightly within tolerance
(`abs_tol=0.001`).

**Fails when:** - The numeric difference exceeds the allowed tolerance.

---

``` python
# abs tol fields in json
@verify_snapshot(abs_tol_fields = {"$.network.*.v": 1.0})
def test_abs_tol_fields():
return '{"network": {"n1": {"v": 10}, "n2": {"v": 10}}}'
```

**Passes when:** - Numeric values of (`v`) differ within tolerance.

**Fails when:** - The numeric difference of (`v`) exceeds the allowed tolerance.

---

``` python
# yaml ignore order
@verify_snapshot(ignore_order_yaml=False)
def test_yaml_order_sensitive():
return """
fruits:
- apple
- banana
"""
```

**Passes when:** - The order of YAML list items is identical.

**Fails when:** - The order changes while order sensitivity is enforced.

---

``` python
# yanl ignore fields
@verify_snapshot(ignore_fields=["age"])
def test_yaml_ignore_fields():
return """
person:
name: Alice
age: 31
city: Paris
"""
```

**Passes when:** - Ignored fields (`age`) differ.

**Fails when:** - Any non-ignored fields differ.

---

``` python
# numeric tolerance
@verify_snapshot(abs_tol=0.02)
def test_yaml_numeric_tolerance():
return """
metrics:
accuracy: 99.96
"""
```

**Passes when:** - Numeric values differ within the given absolute
tolerance.

**Fails when:** - The difference exceeds the allowed tolerance.

---

``` python
@verify_snapshot(
abs_tol_fields={"$.metrics.accuracy": 0.05},
rel_tol_fields={"$.metrics.loss": 0.1},
ignore_order_yaml=True
)
def test_yaml_numeric_tolerances():
return """
metrics:
accuracy: 0.96
loss: 0.105
epoch: 10
"""
```

``` python
# xml numeric tolerance
@verify_snapshot(abs_tol=0.02)
def test_xml_numeric_tolerance():
return "99.96"
```

**Passes when:** - Numeric differences are within tolerance.

**Fails when:** - Values differ by more than the allowed tolerance.

---

``` python
# xml numeric tolerance per field
@verify_snapshot(abs_tol_fields={"//sensor/temp": 0.5})
def test_xml_abs_tol_fields():
return """

20.0
21.0

"""
```

**Passes when:** - Numeric values of (`temp`) differ within tolerance.

**Fails when:** - The numeric difference of (`temp`) exceeds the allowed tolerance.

``` python
import pandas as pd
from pytest_verify import verify_snapshot

# ignore columns
@verify_snapshot(ignore_columns=["B"])
def test_dataframe_ignore_columns():
df = pd.DataFrame({
"A": [1, 4],
"B": [2, 9], # ignored column
"C": [3, 6],
})
return df
```

**Passes when:** - Ignored columns differ (`B`), but all other columns
match.

**Fails when:** - Non-ignored columns differ or structure changes.

---

``` python
import pandas as pd
from pytest_verify import verify_snapshot

@verify_snapshot(abs_tol=0.02)
def test_dataframe_tolerance():
df = pd.DataFrame({
"A": [1.00, 3.00],
"B": [2.00, 4.00],
})
return df
```

**Passes when:** - Numeric differences between runs are within tolerance
(≤ 0.02).

**Fails when:** - Numeric difference exceeds tolerance (e.g. 0.0001).

---

``` python
import numpy as np
from pytest_verify import verify_snapshot

@verify_snapshot(abs_tol=0.01)
def test_numpy_array_tolerance():
return np.array([[1.001, 2.0, 3.0]])
```

**Passes when:** - Element-wise numeric differences are within 0.01.

**Fails when:** - Differences exceed tolerance.

---

``` python
import numpy as np
from pytest_verify import verify_snapshot

@verify_snapshot()
def test_numpy_array_type_mismatch():
return np.array([["1", "2", "3"]], dtype=object)
```

**Passes when:** - Element types match expected (e.g. all numeric).

**Fails when:** - Element types differ (e.g. numeric vs string).

---

``` python
import numpy as np
from pytest_verify import verify_snapshot

@verify_snapshot()
def test_numpy_array_with_none():
return np.array([[1, None, 3]], dtype=object)
```

**Passes when:** - Missing values (None /
NaN) are in the same positions.

**Fails when:** - Missing values occur in different positions or types
differ.

---

## Async Test Support

``pytest-verify`` supports asynchronous (``async def``) test functions in addition to normal synchronous tests.

To enable this feature, you need to install the optional ``async extra``:

pip install pytest-verify[async]

This will install ``pytest-asyncio`` as a test runner integration, allowing ``pytest`` to automatically await async tests.

### Basic Example

``` python

import pytest
from pytest_verify import verify_snapshot

@pytest.mark.asyncio
@verify_snapshot()
async def test_async_snapshot():
await asyncio.sleep(0.01)
return {"status": "ok", "value": 42}
```

### Automatic Async Mode

If you prefer not to use the ``@pytest.mark.asyncio`` decorator on every async test,
you can configure pytest to automatically handle async functions.

Add the following to your ``pytest.ini`` file:
``` ini
[pytest]
asyncio_mode = auto
```
With this setting, async tests work transparently without additional decorators:
``` python

from pytest_verify import verify_snapshot

@verify_snapshot()
async def test_async_auto_mode():
return {"async": True, "auto_mode": "enabled"}
```
Coexistence with Sync Tests
---------------------------

``pytest-verify`` handles both synchronous and asynchronous tests seamlessly.

You can mix both types within the same test suite:

``` python

@verify_snapshot()
def test_sync_snapshot():
return {"type": "sync"}

@pytest.mark.asyncio
@verify_snapshot()
async def test_async_snapshot():
return {"type": "async"}
```
Tips
----

* Use ``ignore_fields`` and numeric tolerances (``abs_tol``, ``rel_tol``) exactly as in sync tests.
* If you see the message:

``You need to install a suitable plugin for your async framework``

it means that no async runner (e.g. ``pytest-asyncio``) is installed.
Fix it by installing the async extra:

```bash
pip install pytest-verify[async]
```
``pytest-verify`` automatically detects whether a test is asynchronous and awaits it safely,
ensuring consistent snapshot creation and comparison across both sync and async workflows.

## Behavior Summary

Step
Description

First run
Creates both .expected and .actual
files.

Subsequent runs
Compares new output with the saved snapshot.

Match found
✅ Snapshot confirmed and updated.

Mismatch detected
⚠️ Shows diff on terminal.

Change accepted
📝 Updates expected snapshot and keeps backup.

---

## Developer Notes

Local installation for development:

pip install -e '.[all]'

Run the test suite:

pytest -v -s

---

## License

Licensed under the **Apache License 2.0**.

---

## Author

**Mohamed Tahri** Email: `simotahri1@gmail.com` GitHub: