1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TL; DR

Unit Test, Integration Testnのコードを書く際、境界値試験のコードを書くことがよくあります。ここではpytest.parametrizeとmath.nextafterを用いたDRY原則に従った簡便な書き方を紹介します。

pytest.parametrize

parametrizeとは

pytestにおけるparametrize(パラメータ化)とは、テスト関数を異なる引数のセットで複数回実行する機能を指します。これは、コードを複製することなく、さまざまな入力値で同じ機能をテストするのに役立ち、テストのカバレッジを向上させます。

パラメータ化の例

以下に、pytestのパラメータ化の仕組みを説明する例を示します:

サンプルコード
import pytest

# テスト対象の関数
def add(a, b):
    return a + b

# テストのパラメータ化
@pytest.mark.parametrize("input_a, input_b, expected", [
    (1, 2, 3),
    (5, 5, 10),
    (-1, -1, -2),
    (0, 0, 0),
    (-1, 1, 0)
])
def test_add(input_a, input_b, expected):
    assert add(input_a, input_b) == expected

サンプルコードの説明

テストのパラメータ化
@pytest.mark.parametrize("input_a, input_b, expected", [
    (1, 2, 3),
    (5, 5, 10),
    (-1, -1, -2),
    (0, 0, 0),
    (-1, 1, 0)
])

このデコレーターは、pytestに対して、異なるパラメータセットでtest_add関数を5回実行するよう指示します。それぞれのタプルの値は

テスト関数
def test_add(input_a, input_b, expected):
    assert add(input_a, input_b) == expected

test_add関数は、リスト内の各タプルに対して1回ずつ実行され、input_a、input_b、expectedは各タプルの値を取ります。

パラメータ化の利点

  • DRY原則: コードの重複を減らします
  • 可読性: テストがより読みやすく、保守しやすくなります
  • 包括的なテスト: さまざまな入力値をテストすることで、カバレッジが広がります

追加のパラメータ化機能

  • 間接パラメータ化: パラメータの初期化方法を制御できます
  • フィクスチャパラメータ化: より複雑なセットアップを作成するためにフィクスチャと組み合わせることができます

math.nextafter

nextafterとは

nextafterは、Pythonのmathモジュールにある関数で、指定した方向に向かってある数値の次の浮動小数点数を見つけるために使用されます。この関数は、数値解析や境界値テストなど、浮動小数点演算を精密に制御する必要があるタスクに特に便利です。

構文

math.nextafter(x: float | int, y: float | int)

パラメータ

  1. x: 開始する浮動小数点数
  2. y: 次の浮動小数点数を見つける方向
    • もしyがxより大きい場合、math.nextafterはxより大きい最小の浮動小数点数を返します
    • もしyがxより小さい場合、xより小さい最大の浮動小数点数を返します

戻り値

関数は、xからyの方向に向かって次の浮動小数点数を返します。パラメーターがint型でもfloatです。

異なるシナリオでmath.nextafterがどのように機能するかを理解するために、いくつかの例を見てみましょう。

与えられた数値より大きい次の浮動小数点数を見つける
import math

# 0.85より大きい最小の浮動小数点数を見つける
result = math.nextafter(0.85, 1.0)
print(result)  # 出力: 0.8500000000000001

この例では、math.nextafter(0.85, 1.0)は0.8500000000000001を返します。これは、0.85より大きい最小の浮動小数点数です。

与えられた数値より小さい次の浮動小数点数を見つける
import math

# 0.15より小さい最大の浮動小数点数を見つける
result = math.nextafter(0.15, 0.0)
print(result)  # 出力: 0.14999999999999999

ここでは、math.nextafter(0.15, 0.0)は0.14999999999999999を返します。これは、0.15より小さい最大の浮動小数点数です。

特殊なケースの処理
import math

# xとyが等しい場合、結果はx
result = math.nextafter(1.0, 1.0)
print(result)  # 出力: 1.0

# 負の数を扱う
result = math.nextafter(-1.0, 0.0)
print(result)  # 出力: -0.9999999999999999

xとyが同じ場合、math.nextafter(x, y)はxを返します。
この関数は負の数にも正しく動作します。

実践

次のような境界値があるとします。

Boundary Value Test.drawio.png

⚪︎はその値を含まず、⚫︎はその値を含むとします。この時テストケースは次の4つになります。

No. 試験値 期待値
1. 0.14999999999999999 LOW
2. 0.15 MIDDLE
3. 0.85 MIDDLE
4. 0.8500000000000001 HIGH

値はpythonのfloat型で表現できる精度としています。

以下のコードは、上記の条件に従ってHIGH, MIDDLE, LOWの文字列を返す関数とそのテストコードになります。

src/parameterizedtest_nextafter/main.py
import math
import pytest

# テスト対象の関数
def classify(a: float) -> str:
    if a < 0.15:
        return "LOW"
    if a <= 0.85:
        return "MIDDLE"
    else:
        return "HIGH"


# テストのパラメータ化
@pytest.mark.parametrize("input, expected", [
    (math.nextafter(0.15, 0.0), "LOW"),
    (0.15, "MIDDLE"),
    (0.85, "MIDDLE"),
    (math.nextafter(0.85, 1.0), "HIGH"),
])
def test_add(input, expected):
    assert classify(input) == expected

このようにすることで、試験条件に合わせて4つのほぼ重複したコードを書くことが避けられ、また0.15未満となる最大の値は何か?とか、0.85を超える最小の値は何かとか考える必要がなくなりました。

以下はpytestの実行結果です。

pytest
% pytest src/parameterizedtest_nextafter/main.py -v
=================================================================================================== test session starts ===================================================================================================
platform darwin -- Python 3.11.5, pytest-7.4.0, pluggy-1.0.0 -- /Users/ttakahashi/anaconda3/bin/python
cachedir: .pytest_cache
rootdir: /Users/ttakahashi/git/parameterizedtest_nextafter
plugins: anyio-3.5.0
collected 4 items                                                                                                                                                                                                         

src/parameterizedtest_nextafter/main.py::test_add[0.14999999999999997-LOW] PASSED                                                                                                                                   [ 25%]
src/parameterizedtest_nextafter/main.py::test_add[0.15-MIDDLE] PASSED                                                                                                                                               [ 50%]
src/parameterizedtest_nextafter/main.py::test_add[0.85-MIDDLE] PASSED                                                                                                                                               [ 75%]
src/parameterizedtest_nextafter/main.py::test_add[0.8500000000000001-HIGH] PASSED                                                                                                                                   [100%]

==================================================================================================== 4 passed in 0.01s ====================================================================================================

無事4つのテストケースが実行されPASSしていることが確認できました。

まとめ

テストコードを書く時、正直言って同じようなコードを書くことは非常に苦痛であり、そのテストコード自体もメンテナンス対象となるため、テストコード量が増えることは技術負債で増えることとも言えます。また、境界値を考えるときに自分の感覚でその値を決めてしまうことはバグを生み出しかねません。
このような状況を避けるためにpytest.parametrizeとmath.nextafterを用いることで、テストコードをシンプルかつ厳密にまとめることができました。

実践のコード

こちらにアップしております。

以上

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?