pytestの辞書比較アダプタRequiredDict
動機
辞書のデータのうち必要なキーだけをガッと比較したい
assert 文をキーの数だけ用意すれば実現できるが、もうちょっと楽をしたい 比較した結果はpytestのdict比較のように見やすく表示したい もうちょっと具体的に
APIのレスポンスでも似たニーズはあるかもしれない
そういった辞書データは多くのキーや入れ子構造のデータを持っている
テストではテストしたいキーだけ確認したいが、assert文をたくさんかくのは面倒
code:test.py
msg = caplog.records0.msg assert msg"message" == f"商品 id={resp.data'id'}, name='テスト' を登録しました。" assert msg"parameters" == {"name": "テスト", "amount": 1234, "owner_id": 1} これだと、4つのassertを行うため2つの問題がある。
期待するデータ構造(右辺)が4行に分散してしまい、把握しづらい。
エラーが出たとき、1つめを直して再実行したら次の行もエラーになる、など1回で把握できない
そこで、↓こうしたい
code:test.py
required = {
"event": "action log",
"request": "POST /",
"message": f"商品 id={resp.data'id'}, name='テスト' を登録しました。", "parameters": {"name": "テスト", "amount": 1234, "owner_id": 1},
}
assert caplog.records0.msg == required しかし、構造化ログのデータには他にも多くのキーが含まれていて、requiredに全部持たせるのは現実的ではない(ログの時刻などmockして比較するのは面倒すぎる)し、そういう値はテストの対象外。
↓のようにassertできるwrapperクラスが欲しい
code:test.py
assert caplog.records0.msg == RequiredDict(required) code:python
>> assert caplog.records0.msg == RequiredDict(required) E AssertionError: assert {'event': 'us...ews', ...} == {'event': 'us..., 'extra': {}}
E Omitting 6 identical items, use -vv to show
E Differing items:
E {'parameters': {'amount': 1234, 'owner_id': 1, 'name': 'テスト'}}
!= {'parameters': {'amount': 12345, 'owner_id': 1, 'name': 'テスト'}}
E {'message': "商品 id=6, name='テスト' を登録しました。"}
!= {'message': "商品 id=1, name='テスト' を登録しました。"}
Omitting 6 identical items とあるように、valueが一致しているデータは表示が省略されている。差分のある parameters と message の2キーはと合わせて8キーが比較されている。 required には4キーしか指定していないので、未指定の4キー分はRequiredDictがうまく吸収してくれている。
実装コード
code:testing.py
class RequiredDict(dict):
"""required dataset for pytest assert comparer
**注意**: selfの辞書データは比較実行時に書き換わるため、RequiredDictインスタンスによる
比較は1度しか行えません。
"""
def __contains__(self, actual_dict: dict) -> bool:
return self.test(actual_dict)
def __eq__(self, actual_dict: dict) -> bool:
return self.test(actual_dict)
def test(self, actual_dict: dict) -> bool:
"""selfのkey,value全てがactual_dictと一致する場合Trueを返す
* self: 必須のkeyとvalueを持つ辞書
* actual_dict: テスト対象のデータ
"""
required_dict = self
required_keys = set(required_dict)
actual_keys = set(actual_dict)
# required_keys ではないactual_dict側の値をselfにコピーし、pytestの結果を見やすくする
for key in actual_keys - required_keys:
required_dictkey = actual_dictkey required_keys = set(required_dict)
# キーの過不足があれば、actual_keysが不足しているため、偽とする
if actual_keys != required_keys:
return False
# キーは一致しているはずなので、値を確認する。
# 再帰を防ぐため、生dictで確認する
return actual_dict == dict(required_dict)
コード解説
class RequiredDict(dict) でdictを継承しているのは、pytestにdictのassert表示をしてもらうため
_pytest.assertion.util:_compare_eq_dict
pytestは型ごとにassert時の表示を調整している
class, sequence, set, dict の調整表示の関数が用意されている
pytest の assertrepr_compare で出力の比較を行っている
この比較間数がprivateなので、pytest pluginを作ったときにこの便利な出力を利用出来ない
独自のassert出力を定義する方法
最初はこの仕組みに乗っかろうとおもったけど、pytestのdict assert実装を流用できないので、dictを継承したwrapperクラスを用意する方向で実現した。