単体テストにて仕様に沿った認識向上を狙っている個人的なやり方

単体テストにて、テストデータが何を意図しているのか個人的に把握しやすくする為に用いている手続きについて書いてみました。
2019.07.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

テストのカバレッジをあげようとテストケースを追加する際、悩ましいのはテスト用データの扱いです。なるだけ網羅させつつも、シンプルに何を意図しているのか分かりやすくしたいという思いで悩むケースがあるかと思います。

規模小さめのテストにおける個人的なやり方になりますが、テストを書こうにも手が付かない・進まないという場合の参考になればとまとめてみました。

まずは正常系テストを一つ書く

正常系テストのルートが荒ぶる程存在する可能性もありますが、とりあえずなにか一つ通します。テストしたい処理には何かしらの引数が存在すると仮定し、今回はそれを仮にdictionary型とします。

import copy


normal = {
    'name': 'AAAAA',
    'age': '30',
}
test_1 = copy.deepcopy(normal)
test_1_result = copy.deepcopy(normal)
test_1_result.update({
    'user_id': '11111',
})
assert my_function(normal) == test_1_result

正常系テスト用データをdeepcopyした上で、出力されるデータ構成へと更新します。色々とネーミングが酷いですが、比較すると結果としてuser_idがくっつく処理になるということが分かります。

例外系テストを追加する

例外扱いとなるテストケースを追加します。

test_2 = copy.deepcopy(normal)
test_2.pop('name')
with pytest.raises(Exception):
    my_function(test_1)

name要素が削除されているとエラーになることが分かります。

類似したケースをまとめて追加する

pytest.mark.parametrizeを利用します。

import pytest
import copy


@pytest.mark.parametrize("source_diff, result_diff, is_valied", [
    ({}, {'id': '4'}, True),
    ({'name': 'tes'}, None, False)
])
def test_my_function_second(source_diff, result, is_valied):
    source = {'name': 'test', 'mail': 'x@mm'}
    record = copy.deepcopy(source)
    record.update(source_diff)
    if not is_valied:
        with pytest.raises(Exception):
            my_function_second(record)
    else:
        result = copy.deepcopy(source)
        result.update(result_diff)
        assert my_function_second(record) == result

共通のリソースを処理に埋め込み、差分をparametrizeから渡しています。元のデータが小さめの場合は差分にしないほうが見通しはよくなります。

あとがき

変更差分をもたせるやり方は、単体テストでの仕様が明確な前提で個人的に把握の負担が減らしたいために使っている方法です。結合テスト等、データを並べてdiffを取ることで確認したい場合には向かない方法だと思います。

多分独特すぎる方法だと思っていますが、データ入力が手間な場合や目grepが辛いケースにて、問題がなければ試してみると楽になるかもしれません。