単体テストを書いていて、
入力値の組み合わせを網羅しようとすると、
記述が増えて、テストコードの保守がしにくいと思ったので、
もうちょっと機械的に書けないかを考えてみました。

手動でテストをする場合は、いくつかピックアップして実施しますが。
自動テストで、時間がかからないのであれば、
全パターン網羅してしまった方が楽だと思います(気分的にも)。

このエントリではpytestを使って、考えてみます。

テスト対象

まずは、テスト対象のコードを示します。

def target(x1, x2, x3):
    if x1:
        if x2:
            if x3:
                return True
    return False

このコードに対する単体テストを網羅しようとすると、
次のような決定表を作り、

決定表

#x1x2x2y
1TrueTrueTrueTrue
2TrueTrueFalseFalse
3TrueFalseTrueFalse
4TrueFalseFalseFalse
5FalseTrueTrueFalse
6FalseTrueFalseFalse
7FalseFalseTrueFalse
8FalseFalseFalseFalse

次のようなテストコードを書くことになるかと思います。

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}"

以上。