Pytestで直積とって単体テストを流すことを考えてみた
単体テストを書いていて、
入力値の組み合わせを網羅しようとすると、
記述が増えて、テストコードの保守がしにくいと思ったので、
もうちょっと機械的に書けないかを考えてみました。
手動でテストをする場合は、いくつかピックアップして実施しますが。
自動テストで、時間がかからないのであれば、
全パターン網羅してしまった方が楽だと思います(気分的にも)。
このエントリではpytestを使って、考えてみます。
テスト対象
まずは、テスト対象のコードを示します。
def target(x1, x2, x3):
if x1:
if x2:
if x3:
return True
return False
このコードに対する単体テストを網羅しようとすると、
次のような決定表を作り、
決定表
# | x1 | x2 | x2 | y |
---|---|---|---|---|
1 | True | True | True | True |
2 | True | True | False | False |
3 | True | False | True | False |
4 | True | False | False | False |
5 | False | True | True | False |
6 | False | True | False | False |
7 | False | False | True | False |
8 | False | False | False | False |
次のようなテストコードを書くことになるかと思います。
import pytest
@pytest.mark.parametrize("x1, x2 ,x3 ,y", [
(True, True, True, True),
(True, True, False, False),
(True, False, True, False),
(True, False, False, False),
(False, True, True, False),
(False, True, False, False),
(False, False, True, False),
(False, False, False, False),
])
def test_target_all(x1, x2, x3, y):
assert target(x1, x2, x3) == y
これはこれで分かりやすくて良いのですが、
組み合わせパターンが増えるとちょっとしんどいです。
pytest.parametrizeを使う
pytest.mark.parametrizeを使うケースで考えてみます。
pytest.mark.parametrizeを複数書くと、
直積を取って、各パターンのテストを実施してくれるので。
例えば、次のように書くことが出来ます。
import pytest
@pytest.mark.parametrize("x1", [True, False])
@pytest.mark.parametrize("x2", [True, False])
@pytest.mark.parametrize("x3", [True, False])
def test_target_cartesian_parametrize(x1, x2, x3):
import inspect
expect_positive_set = [
{"x1": True, "x2": True, "x3": True},
]
rtn = target(x1, x2, x3)
xargs = eval("}".format(','.join([
f"'{k}':{k}"
for k in inspect.currentframe().f_code.co_varnames[:inspect.currentframe().f_code.co_argcount]
])))
assert rtn == (True in [x == {k: xargs[k] for k in x.keys()} for x in expect_positive_set])
expect_positive_setにある組み合わせのみTrueの返却を期待し、
それ以外はFalseの返却を期待します。
このエントリで考えたテスト対象だと、
Trueを返却するパターンは限られているので、
この書き方の方が、人が見てわかりやすいと思います。
itertools.productを使う
次は、itertools.productで直積とるケースです。
pytest.mark.parametrizeと同じですが、
テスト関数の呼び出しは1回で出来るので、
(バッチ系の処理でよくある)
テスト対象の関数に、パラメータをリストで渡せるような場合は、
こちらの方が効率良いと思います。
def test_target_cartesian_itertools():
import itertools
test_values = {
'x1': [True, False],
'x2': [True, False],
'x3': [True, False],
}
expect_positive_set = [
{"x1": True, "x2": True, "x3": True},
]
test_set = [dict(zip(test_values.keys(), v)) for v in itertools.product(*(test_values.values()))]
for xargs in test_set:
rtn = target(**xargs)
assert rtn == (True in [x == {k: xargs[k] for k in x.keys()} for x in expect_positive_set]), f"case={xargs}"
ちなみにリストで渡すケースは、こんな感じになると思います。
def target(x1, x2, x3):
if x1:
if x2:
if x3:
return True
return False
def target_list(params):
return [target(**v) for v in params]
def test_target_cartesian_itertools_list():
import itertools
test_values = {
'x1': [True, False],
'x2': [True, False],
'x3': [True, False],
}
expect_positive_set = [
{"x1": True, "x2": True, "x3": True},
]
test_set = [dict(zip(test_values.keys(), v)) for v in itertools.product(*(test_values.values()))]
rtn_list = target_list(test_set)
for xargs, rtn in zip(test_set, rtn_list):
assert rtn == (True in [x == {k: xargs[k] for k in x.keys()} for x in expect_positive_set]), f"case={xargs}"
以上。