【Python】テストの可読性を損なう「共有フィクスチャ」の罠:テストデータはテストケース内に書こう

テストコードを書いていると、「このリスト、他のテストでも使うから共通化しよう」と考え、共有のファイルや関数にテストデータを切り出したくなることがあります。コードのDRY(Don’t Repeat Yourself)原則に従っているようで、一見すると良いプラクティスに見えるかもしれません。

しかし、テストのデータを複数のテストケースで共有することは、**「テスト間の結合」**という深刻な問題を引き起こし、テストスイート全体を脆く、理解しにくいものにしてしまいます。

優れたテストの原則は、**「テストケースは自己完結しているべき」**というものです。今回は、なぜテストデータを共有すべきではないのか、そしてどのようにして各テストを独立させるべきかを解説します。

目次

問題点:共有データが引き起こす「遠隔作用」

ある数値リストの各要素を二乗する関数square_numbersをテストするシナリオを考えてみましょう。

app/math_utils.py

def square_numbers(numbers: list[int]) -> list[int]:
    return [n * n for n in numbers]

ここで、テストデータ[1, 2, 3]を、将来他のテストでも使うかもしれないと考え、共有ファイルに切り出してしまいました。

tests/shared.py

def get_sample_numbers():
    return [1, 2, 3]

悪い例:外部の共有データに依存したテスト tests/test_math_utils.py

from app.math_utils import square_numbers
from .shared import get_sample_numbers

def test_square_numbers():
    # Arrange: テストデータが外部ファイルに隠蔽されている
    test_data = get_sample_numbers()

    # Act
    actual = square_numbers(test_data)

    # Assert
    assert actual == [1, 4, 9]

このテストの問題点は、その脆さにあります。 もし別の開発者が、自身の新しいテストのためにget_sample_numbers[1, 2, 3, 4]に変更したらどうなるでしょうか? test_square_numbersは、そのコードには一切変更がないにもかかわらず、突然失敗し始めます。

このように、ある場所での変更が、遠く離れた無関係に見えるテストを破壊する現象は**「遠隔作用(Action at a Distance)」**と呼ばれ、テストが不安定になる主な原因です。また、テストの意図を理解するために、shared.pyファイルを開いてデータの中身を確認しに行く手間も発生します。


解決策①:シンプルなデータはテスト内に直接定義する

最もシンプルで堅牢な解決策は、テストに必要なデータを、そのテストケースの内部で直接定義することです。

良い例:テストデータが自己完結している

from app.math_utils import square_numbers

def test_square_numbers():
    # Arrange: このテストに必要なデータは、この場所で完結している
    test_data = [1, 2, 3]

    # Act
    actual = square_numbers(test_data)

    # Assert
    assert actual == [1, 4, 9]

このテストは、他のどのファイルにも依存しておらず、完全に独立しています。テストコードを読むだけで、どのような入力に対してどのような出力を期待しているかが一目瞭然です。


解決策②:Factoryを正しく使う

モデルオブジェクトのような複雑なデータの場合、factory-boyのようなファクトリを使うのが一般的です。しかし、ファクトリも使い方を誤ると、共有データと同じ問題を引き起こします。

良くない使い方:ファクトリのデフォルト値に依存する ファクトリがusername="default-user"というデフォルト値を生成するとします。

# 良くない:テストがファクトリの「デフォルト値」という実装の詳細に依存している
def test_find_user_by_name():
    user = UserFactory() # デフォルト名 "default-user" で作成されると仮定
    found = find_user_by_name("default-user") # "マジックストリング"
    assert found.id == user.id

もし誰かがUserFactoryのデフォルト名を変更したら、このテストは壊れてしまいます。

Factoryのより良い使い方

方法A:テストで必要な値は明示的に指定する

# 良い:このテストで使うユーザー名を明示的に指定する
def test_find_user_by_name_explicit():
    user_name_for_test = "user-for-this-specific-test"
    user = UserFactory(username=user_name_for_test)
    
    found = find_user_by_name(user_name_for_test)
    assert found.id == user.id

方法B:生成されたオブジェクトの値を参照する

# 良い:「マジックストリング」を避け、生成された値を使って一貫性を検証する
def test_find_user_by_name_consistent():
    user = UserFactory()
    found = find_user_by_name(user.username)
    assert found.id == user.id

これらの改善により、テストはファクトリのデフォルト実装から切り離され、より自己完結し、堅牢になります。

まとめ

テストコードの信頼性と可読性を高めるための重要な原則は、**「テストケースごとに、そのテストのためだけのデータを用意する」**ことです。

  • **シンプルなデータ(リスト、辞書など)**は、テストメソッド内に直接記述する。
  • 複雑なオブジェクト(モデルインスタンスなど)はファクトリで生成するが、テストの検証に使う値は明示的に指定するか、生成されたオブジェクトから取得する

テストコードの共通化は、ロジックやヘルパー関数に対して行うべきであり、テストケースごとの具体的な「データ」を共有するのは避けるべきアンチパターンです。各テストを独立した自己完備なものにすることで、誰にとっても理解しやすく、壊れにくいテストスイートを構築しましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次