📝
pytestでスナップショットテスト syrupy
普段使ってるツールの使ってない機能を使ってみる Advent Calendar 2025です。
syrupyとは
pytestでSnapshotテストを実行するプラグインです。
GitHubリポジトリでは、
Syrupy is a zero-dependency pytest snapshot plugin. It enables developers to write tests which assert immutability of computed results.
と説明されています。
snapshotテストとは
定義や歴史見つけられてないのですが、(1)過去のある時点の実行結果と、(2)コード変更後の実行結果を比較するテストだと思っています。
pytestに限らず、CDKやPlaywright等で出てきます。
試してみる
シンプルな場合
適当なテスト対象クラスを作ります。
class Calculator:
def calculate(self, a, b, op):
"""基本的な計算結果を辞書で返す"""
result = {
"+": a + b,
"-": a - b,
"*": a * b,
"/": a / b if b != 0 else None
}.get(op)
return {
"operation": f"{a} {op} {b}",
"result": result,
"operands": [a, b],
"status": "success", # 新しいフィールドを追加
}
syrupyを使うには、
- testメソッドのパラメータでsnapshotを受け取る
- test対象の処理を実行
- (1)と(2)を比較
するだけです。
import pytest
from unittest.mock import patch
from calculator import Calculator
@pytest.fixture
def calc():
return Calculator()
def test_basic_snapshot(snapshot, calc):
"""辞書のスナップショットテスト"""
result = calc.calculate(10, 5, "+")
assert result == snapshot
一回目はスナップショットがないので失敗します。
> pytest -k test_basic_snapshot
============================================================================================== test session starts ==============================================================================================
platform linux -- Python 3.11.11, pytest-9.0.1, pluggy-1.6.0
rootdir: /home/notrogue/project/pytest/pytest_sandbox
configfile: pyproject.toml
plugins: testmon-2.2.0, syrupy-5.0.0
collected 10 items / 9 deselected / 1 selected
test_calculator_snapshots.py F [100%]
=================================================================================================== FAILURES ====================================================================================================
______________________________________________________________________________________________ test_basic_snapshot ______________________________________________________________________________________________
snapshot = SnapshotAssertion(name='snapshot', num_executions=1), calc = <calculator.Calculator object at 0x70a83bcf8b10>
def test_basic_snapshot(snapshot, calc):
"""辞書のスナップショットテスト"""
result = calc.calculate(10, 5, "+")
> assert result == snapshot
E AssertionError: assert [+ received] == [- snapshot]
E Snapshot 'test_basic_snapshot' does not exist!
E + dict({
E + 'operan
E
E ...Full output truncated (8 lines hidden), use '-vv' to show
test_calculator_snapshots.py:16: AssertionError
-------------------------------------------------------------------------------------------- snapshot report summary --------------------------------------------------------------------------------------------
1 snapshot failed.
============================================================================================ short test summary info ============================================================================================
FAILED test_calculator_snapshots.py::test_basic_snapshot - AssertionError: assert [+ received] == [- snapshot]
--snapshot-updateパラメータを付けて実行すると、その実行の結果を正としてスナップショットを保存します。
pytest -k test_basic_snapshot --snapshot-update
もう一回実行すると、今度は実行結果とスナップショットが比較出来て(同じ値なので当然)成功します。
pytest -k test_basic_snapshot --snapshot-update
============================================================================================== test session starts ==============================================================================================
platform linux -- Python 3.11.11, pytest-9.0.1, pluggy-1.6.0
rootdir: /home/notrogue/project/pytest/pytest_sandbox
configfile: pyproject.toml
plugins: testmon-2.2.0, syrupy-5.0.0
collected 10 items / 9 deselected / 1 selected
test_calculator_snapshots.py . [100%]
-------------------------------------------------------------------------------------------- snapshot report summary --------------------------------------------------------------------------------------------
1 snapshot passed.
======================================================================================
スナップショットは__snapshot__ディレクトリに保存されます。
> cat __snapshots__/test_calculator_snapshots.ambr
# serializer version: 1
# name: test_basic_snapshot
dict({
'operands': list([
10,
5,
]),
'operation': '10 + 5',
'result': 15,
'status': 'success',
})
# ---
変更を検知してみる
スナップショットで変更を検知できること確認するために、足し算を文字列の連結に変えてみます。
"+": str(a) + str(b),
> pytest -k test_basic_snapshot
============================================================================================== test session starts ==============================================================================================
platform linux -- Python 3.11.11, pytest-9.0.1, pluggy-1.6.0
rootdir: /home/notrogue/project/pytest/pytest_sandbox
configfile: pyproject.toml
plugins: testmon-2.2.0, syrupy-5.0.0
collected 10 items / 9 deselected / 1 selected
test_calculator_snapshots.py F [100%]
=================================================================================================== FAILURES ====================================================================================================
______________________________________________________________________________________________ test_basic_snapshot ______________________________________________________________________________________________
snapshot = dict({
'operands': list([
10,
5,
]),
'operation': '10 + 5',
'result': 15,
'status': 'success',
}), calc = <calculator.Calculator object at 0x7f935aa99110>
def test_basic_snapshot(snapshot, calc):
"""辞書のスナップショットテスト"""
result = calc.calculate(10, 5, "+")
> assert result == snapshot
E AssertionError: assert [+ received] == [- snapshot]
E dict({
E ...
E 'operation': '10 + 5',
E - 'result': 15,
E
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
test_calculator_snapshots.py:16: AssertionError
-------------------------------------------------------------------------------------------- snapshot report summary --------------------------------------------------------------------------------------------
1 snapshot failed.
============================================================================================ short test summary info ============================================================================================
FAILED test_calculator_snapshots.py::test_basic_snapshot - AssertionError: assert [+ received] == [- snapshot]
検知できてますね。
呼び出しのスナップショット
(たぶん)直接的にはスナップショット対象ではないのですが、下のようにpatchしてcall_args_listをスナップショットして保存すると、呼び出し内容もスナップショットテストできます。
def test_print(snapshot, calc):
with patch('builtins.print') as mock_print:
result = calc.print()
print_calls = [str(call) for call in mock_print.call_args_list]
assert result == snapshot(name="result")
assert print_calls == snapshot(name="print")
Discussion