ユニットテストの目的は、コードが正しく動作することを確認するだけではありません。テストが失敗したときに、**「何が、どのように、なぜ間違っているのか」**を迅速かつ明確に開発者に伝えることも、同様に重要な役割です。
この「診断能力」の高いテストを書くための指針として、**「1テストメソッドでは、1つの論理的な関心事のみを検証する」**という原則があります。しばしば「1テスト1アサーション」とも呼ばれるこの原則に従うことで、テストは遥かに価値のあるものになります。今回は、この原則の重要性を、具体的なリファクタリングを通して見ていきましょう。
問題点:多くの責務を抱えたテストメソッド
ユーザー名が有効かどうかを検証するis_valid_username関数をテストするシナリオを考えます。有効なユーザー名は「4文字以上、20文字以下」とします。
validators.py
def is_valid_username(username: str) -> bool:
    """ユーザー名が4文字以上、20文字以下であればTrueを返す。"""
    return 4 <= len(username) <= 20
この関数に対し、正常系・異常系すべての検証を一つのテストメソッドに詰め込むと、次のようになります。
悪い例:一つのテストに複数の関心事が混在
# tests/test_validators.py
from validators import is_valid_username
def test_is_valid_username():
    # 正常系(境界値、中間値)
    assert is_valid_username("abcd") # 4文字
    assert is_valid_username("a" * 12) # 12文字
    assert is_valid_username("a" * 20) # 20文字
    
    # 異常系(短すぎる)
    assert not is_valid_username("abc") # 3文字
    
    # 異常系(長すぎる)
    assert not is_valid_username("a" * 21) # 21文字
このテストには、主に2つの問題があります。
- 曖昧な失敗レポート: もしこのテストが失敗した場合、テストランナーは単に
test_is_valid_usernameが失敗したことしか教えてくれません。「短すぎた」のか「長すぎた」のか、あるいは「正常なはずのものが弾かれた」のか、失敗の具体的な原因を特定するために、コードの内部まで見に行く必要があります。 - エラーのマスキング: テストは、アサーションが一つでも失敗した時点で実行を停止します。例えば、
assert not is_valid_username("abc")で失敗した場合、その後の「長すぎる」ケースの検証は実行されません。一つのバグを修正したら、再実行して初めて次のバグに気づく、という非効率なサイクルに陥ります。 
解決策①:振る舞いごとにテストメソッドを分割する
この問題を解決する最も基本的な方法は、検証したい「振る舞い」や「仕様」ごとにテストメソッドを分割することです。
良い例:振る舞いに基づいてテストを分割
def test_username_is_valid_with_boundary_and_normal_lengths():
    """正常系の長さ(4文字、20文字、中間)でTrueを返すことを確認する。"""
    assert is_valid_username("abcd")
    assert is_valid_username("a" * 12)
    assert is_valid_username("a" * 20)
def test_username_is_invalid_when_too_short():
    """仕様より短いユーザー名でFalseを返すことを確認する。"""
    assert not is_valid_username("abc")
def test_username_is_invalid_when_too_long():
    """仕様より長いユーザー名でFalseを返すことを確認する。"""
    assert not is_valid_username("a" * 21)
このリファクタリングにより、テストの失敗レポートは劇的に改善されます。例えば、test_username_is_invalid_when_too_shortが失敗すれば、テスト名だけで「短すぎるケースの検証で問題が発生した」ことが一目瞭然です。テストが、それ自体で仕様書のように振る舞い始めます。
解決策②(モダンなアプローチ):pytest.mark.parametrizeでケースをまとめる
振る舞いごとにテストを分けるのは良いアプローチですが、似たようなテストケース(例えば、正常系の3パターン)のために別々のメソッドを書くのは冗長に感じるかもしれません。
このような場合、モダンなテストフレームワークであるpytestのparametrize機能が非常に強力です。これにより、一つのテストメソッドのロジックを、複数の異なる入力データで実行できます。
さらに良い例:parametrizeで関連ケースを整理
import pytest
from validators import is_valid_username
@pytest.mark.parametrize(
    "valid_username", ["abcd", "a" * 12, "a" * 20]
)
def test_is_valid_username_valid_cases(valid_username):
    """正常系のユーザー名でTrueを返すことを確認する。"""
    assert is_valid_username(valid_username)
@pytest.mark.parametrize(
    "invalid_username", ["abc", "a" * 21, ""]
)
def test_is_valid_username_invalid_cases(invalid_username):
    """異常系のユーザー名でFalseを返すことを確認する。"""
    assert not is_valid_username(invalid_username)
このアプローチは、両方の解決策の「良いとこ取り」です。
- 関心事の分離: 
valid_casesとinvalid_casesという、論理的なまとまりでテストが分離されています。 - コードのDRY原則: 似たようなテストケースのための冗長なコードがなくなりました。
 - 明確な失敗レポート: 
pytestは、どのパラメータ(例:"a" * 21")でテストが失敗したかを正確に報告してくれるため、診断能力も非常に高いです。 
まとめ
診断能力の高い、価値あるユニットテストを書くための指針はシンプルです。
- 1つのテストメソッドでは、1つの論理的な振る舞いや仕様のみを検証する。
 - テストメソッドの名前は、「何をテストしているか」が明確にわかるように命名する。
 - 同じ振る舞いに対する複数のテストケースは、
pytest.mark.parametrizeを使って整理する。 
この原則に従うことで、テストは単なる「動くかどうかの確認」から、アプリケーションの仕様を物語る生きたドキュメントへと昇華し、未来の開発における強力な味方となります。
