pytestのparametrizeでdataclassを利用してみる

データクラス名やキーワード引数名を明示できるため、テストコードを見たときに分かりやすくなることが期待できます。
2024.05.14

pytestのparametrizeを活用すれば、ひとつのテストメソッドに対して、パラメータを変えるテストが便利に書けます。

しかし、parametrizeのパラメータの数が多かったり、数は少なくとも内容が多いとき、テストコードが縦に長くなって見づらくなることがあります。 改善方法を考えているとき、ふと、「そういえば、pytestのparametrizeでdataclassが使えたりするかな?」と閃いたので、試してみました。

おすすめの方

  • pytestのparametrizeでdataclassを利用したい方

テストパターンが見づらい例

たとえば、SNSトピックを受け取って処理するLambdaのUnit Testを書く場合を想定します。 パラメータの量や内容が多いと、途中で「これはどれが何だっけ?」「user_info と device_info は、どっちが先だっけ?」みたいになることがあるのです。

test_sample.py

import pytest
import app
import json


@pytest.mark.parametrize(
    "event, user_info, device_info, xxx, expected",
    [
        # パターン1
        (
            # 先頭なので、これは event だと分かりやすい
            {
                "Records": [
                    {
                        "body": json.dumps(
                            {
                                "Message": json.dumps(
                                    {
                                        "aaa": "111",
                                        "bbb": "222",
                                        "aaa": "333",
                                    }
                                )
                            }
                        )
                    },
                    {
                        "body": json.dumps(
                            {
                                "Message": json.dumps(
                                    {
                                        "aaa": "AAA",
                                        "bbb": "BBB",
                                        "aaa": "CCC",
                                    }
                                )
                            }
                        )
                    },
                    {
                        "body": json.dumps(
                            {
                                "Message": json.dumps(
                                    {
                                        "aaa": "xxx",
                                        "bbb": "yyy",
                                        "aaa": "zzz",
                                    }
                                )
                            }
                        )
                    },
                ]
            },
            # これはどれが何だっけ?
            {
                # 略
            },
            {
                # 略
            },
            {
                # 略
            },
            {
                # 略
            },
        ),
        # パターン2
        (
            {
                "Records": [
                    {
                        "body": json.dumps(
                            {
                                "Message": json.dumps(
                                    {
                                        "aaa": "111",
                                        "bbb": "222",
                                        "aaa": "333",
                                    }
                                )
                            }
                        )
                    },
                    {
                        "body": json.dumps(
                            {
                                "Message": json.dumps(
                                    {
                                        "aaa": "AAA",
                                        "bbb": "BBB",
                                        "aaa": "CCC",
                                    }
                                )
                            }
                        )
                    },
                    {
                        "body": json.dumps(
                            {
                                "Message": json.dumps(
                                    {
                                        "aaa": "xxx",
                                        "bbb": "yyy",
                                        "aaa": "zzz",
                                    }
                                )
                            }
                        )
                    },
                ]
            },
            # user_info と device_info は、どっちが先だっけ?
            {
                # 略
            },
            {
                # 略
            },
            {
                # 略
            },
            # これは何だっけ?
            {
                # 略
            },
        ),
    ],
)
def test_message(event, user_info, device_info, xxx, expected):
    # actual = app.main(event, None)
    # assert actual == expected
    pass

dataclassを利用してみる(短い版)

Messageという名前でデータクラスを作成して、parametrizeで利用しています。

test_sample_use_dataclass1.py

import pytest
import app

from dataclasses import dataclass


@dataclass
class Message(object):
    text1: str
    text2: str


@pytest.mark.parametrize(
    "message, expected",
    [
        (Message("hello", "world"), "hello world"),  # キーワード引数なし
        (Message(text1="FOO", text2="BAR"), "FOO BAR"),  # キーワード引数あり
    ],
)
def test_message_use_data_class(message, expected):
    actual = app.message1(message.text1, message.text2)
    assert actual == expected

dataclassを利用してみる(長い版)

MessageParamsという名前でデータクラスを作成して、parametrizeで利用しています。

データクラスの定義(内容)はいろいろな方針があると思います。

  • ひとつのデータクラスで、すべてのパラメータを扱えるようにする
  • expectedとそれ以外のデータクラスを作成する(下記の例がこれ)
  • それぞれのパラメータごとにデータクラスを作成する
  • など

他でも使い回せるか?などを考慮しつつ、自分たちにとって分かりやすい&都合が良い方針を採用すると良いですね。

test_sample_use_dataclass2.py

import pytest
import app
import json

from dataclasses import dataclass


@dataclass
class MessageParams(object):
    event: dict
    user_info: dict
    device_info: dict
    xxx: dict


@dataclass
class Expected(object):
    expected: dict


@pytest.mark.parametrize(
    "message_params, expected",
    [
        # パターン1
        (
            MessageParams(
                event={
                    "Records": [
                        {
                            "body": json.dumps(
                                {
                                    "Message": json.dumps(
                                        {
                                            "aaa": "111",
                                            "bbb": "222",
                                            "aaa": "333",
                                        }
                                    )
                                }
                            )
                        },
                        {
                            "body": json.dumps(
                                {
                                    "Message": json.dumps(
                                        {
                                            "aaa": "AAA",
                                            "bbb": "BBB",
                                            "aaa": "CCC",
                                        }
                                    )
                                }
                            )
                        },
                        {
                            "body": json.dumps(
                                {
                                    "Message": json.dumps(
                                        {
                                            "aaa": "xxx",
                                            "bbb": "yyy",
                                            "aaa": "zzz",
                                        }
                                    )
                                }
                            )
                        },
                    ]
                },
                user_info={
                    # 略
                },
                device_info={
                    # 略
                },
                xxx={
                    # 略
                },
            ),
            Expected(
                {
                    # 略
                }
            ),
        ),
        # パターン2
        (
            MessageParams(
                event={
                    "Records": [
                        {
                            "body": json.dumps(
                                {
                                    "Message": json.dumps(
                                        {
                                            "aaa": "111",
                                            "bbb": "222",
                                            "aaa": "333",
                                        }
                                    )
                                }
                            )
                        },
                        {
                            "body": json.dumps(
                                {
                                    "Message": json.dumps(
                                        {
                                            "aaa": "AAA",
                                            "bbb": "BBB",
                                            "aaa": "CCC",
                                        }
                                    )
                                }
                            )
                        },
                        {
                            "body": json.dumps(
                                {
                                    "Message": json.dumps(
                                        {
                                            "aaa": "xxx",
                                            "bbb": "yyy",
                                            "aaa": "zzz",
                                        }
                                    )
                                }
                            )
                        },
                    ]
                },
                user_info={
                    # 略
                },
                device_info={
                    # 略
                },
                xxx={
                    # 略
                },
            ),
            Expected(
                {
                    # 略
                },
            ),
        ),
    ],
)
def test_message(message_params, expected):
    # actual = app.main(message_params.event, None)
    # assert actual == expected.expected
    pass

さいごに

見通しが良いテストコードには過剰かもしれません。使い所には気をつけていきたいですね。

参考