取りあえず何か創る

ソフトウェアエンジニアだけど仕事以外でも何か作りたくなったので、主にその記録のためにブログを開設しました。

pytestでテスト入力をテスト関数間で共有する

0. この投稿の概要

  • クラスメソッドをテストする時に、同じ入力を異なるメソッドのテストで使いまわしたいと思うことがあったので、pytestでの実現方法を調べてみました。
  • 今回はfixtureとparametrizeを組み合わせた実装を紹介します。多分色々方法はあるかと思いますので、一例として参考になればと思います。

1. 実装例

テスト対象のクラス

  • 初期化時に変数を2つ受け取り、メソッドで足し算や掛け算結果を返すという単純なクラスです。
import pytest

class Calculator:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def add(self):
        return self.a + self.b

    def mul(self):
        return self.a * self.b

    def add_c(self, c):
        return self.a + self.b + c

パターン1: 1つのテスト関数で複数の関数をテストする。

  • parametrizeに複数の関数の期待値を記載する方法です。
  • 最も単純なやり方ではありますが、柔軟性がないのでadd_cのように別途引数がある場合などでは他の関数に無駄なテストが追加されてしまいます。
  • 今回は引数も期待値も整数型なので問題にはなりませんが、これがリストや辞書など大きな入力になると視認性も悪くなりそうです。
@pytest.mark.parametrize(
    ("a", "b", "add_expected", "mul_expected"),
    [(1, 2, 3, 2), (3, 4, 7, 12)],
)
def test_all(a, b, add_expected, mul_expected):
    assert add_expected == Calculator(a, b).add()
    assert mul_expected == Calculator(a, b).mul()

パターン2: parametrizeのindirectを使う。

  • parametrizeのindirectオプションを使うと、指定したパラメータの値を受け取ったfixtureを使うことができます。
  • 下の例ではfixture calc_instanceに"test1"や"test2"といった値がrequest.param経由で渡されます。それを受けてパラメータを切り替えたインスタンスを異なるテスト関数に渡すことができます。
  • パターン1よりも柔軟にテストを作成できると思います。test_add_cではfixture以外の引数cも別途指定しています。
@pytest.fixture
def calc_instance(request):
    config_dict = {"test1": {"a": 1, "b": 2}, "test2": {"a": 3, "b": 4}}
    config = config_dict[request.param]
    return Calculator(config["a"], config["b"])


@pytest.mark.parametrize(
    ("calc_instance", "expected"),
    [("test1", 3), ("test2", 7)],
    indirect=["calc_instance"],
)
def test_add(calc_instance, expected):
    assert expected == calc_instance.add()


@pytest.mark.parametrize(
    ("calc_instance", "expected"),
    [("test1", 2), ("test2", 12)],
    indirect=["calc_instance"],
)
def test_mul(calc_instance, expected):
    assert expected == calc_instance.mul()


@pytest.mark.parametrize(
    ("calc_instance", "c", "expected"),
    [("test1", 1, 4), ("test2", -1, 6)],
    indirect=["calc_instance"],
)
def test_add_c(calc_instance, c, expected):
    assert expected == calc_instance.add_c(c)

参考:
docs.pytest.org

パターン3: getfixturevalueを使う。

  • getfixturevalueにfixture名を渡せばそのfixtureが返ってきます。
  • 下の例ではfixture名"test3"や"test4"をparametrizeで渡しています。
  • 今回の簡単な例ではパターン2とパターン3ではほぼ同じような使い勝手です。もっと複雑な例では違いが出てくるかもしれません。
@pytest.fixture
def test3():
    return Calculator(1, 3)


@pytest.fixture
def test4():
    return Calculator(2, 4)


@pytest.mark.parametrize(
    ("calc_instance", "expected"),
    [("test3", 4), ("test4", 6)],
)
def test_add2(calc_instance, expected, request):
    calc = request.getfixturevalue(calc_instance)
    assert expected == calc.add()


@pytest.mark.parametrize(
    ("calc_instance", "expected"),
    [("test3", 3), ("test4", 8)],
)
def test_mul2(calc_instance, expected, request):
    calc = request.getfixturevalue(calc_instance)
    assert expected == calc.mul()


@pytest.mark.parametrize(
    ("calc_instance", "c", "expected"),
    [("test3", 1, 5), ("test4", -1, 5)],
)
def test_add_c2(calc_instance, c, expected, request):
    calc = request.getfixturevalue(calc_instance)
    assert expected == calc.add_c(c)

参考:
docs.pytest.org