関数が返すリストの内容をテストする際、特定の要素が含まれているかを確認するアサーションを書くのは自然なことです。しかし、リストに含まれる要素の「総数」を確認するアサーションを忘れてはいないでしょうか?
この一見些細な確認を怠ると、予期せぬ要素がリストに紛れ込んでいてもテストがパスしてしまう、「偽陽性(False Positive)」の罠にはまる危険性があります。今回は、なぜ要素数のテストが不可欠なのか、そしてどのようにして完全性を検証すべきかを解説します。
問題点:予期せぬ要素を見逃す「穴だらけ」のテスト
アクティブなユーザーのみをフィルタリングして返す関数get_active_users
を考えてみましょう。
テスト対象のコード (services.py
):
from dataclasses import dataclass
@dataclass
class User:
name: str
is_active: bool
def get_active_users(all_users: list[User]) -> list[User]:
"""アクティブなユーザーのリストを返す。"""
# わざとバグを混入: `is_active`ではなく、nameが空でないかでフィルタリングしている
return [user for user in all_users if user.name]
この関数には、「is_active
フラグを見るべきところを、誤ってname
の有無で見ている」というバグが潜んでいます。
この関数に対し、要素数の確認を怠ったテストを書いてみましょう。
悪い例:要素数を確認していないテスト
# tests/test_services.py
def test_get_active_users_without_len_check():
# Arrange: 3人のユーザーを用意(Bobは非アクティブ)
users = [
User(name="Alice", is_active=True),
User(name="Bob", is_active=False), # このユーザーは除外されるべき
User(name="Charlie", is_active=True),
]
# Act
active_users = get_active_users(users)
# Assert: 期待するユーザーが含まれていることだけを確認
assert User(name="Alice", is_active=True) in active_users
assert User(name="Charlie", is_active=True) in active_users
このテストは成功してしまいます。バグのあるget_active_users
関数は、is_active
がFalse
の”Bob”もname
があるので返してしまいますが、テストは”Alice”と”Charlie”がいることしか確認していないため、”Bob”という余計な要素の存在を見逃してしまうのです。
解決策①:要素数を明示的にテストする
この問題を解決する最も直接的な方法は、リストの要素数をlen()
で確認するアサーションを追加することです。
良い例:len()
で要素数を検証する
def test_get_active_users_with_len_check():
# ... Arrange, Act は同じ ...
users = [
User(name="Alice", is_active=True),
User(name="Bob", is_active=False),
User(name="Charlie", is_active=True),
]
active_users = get_active_users(users)
# Assert:
# 1. まず、期待される要素の総数を確認する
assert len(active_users) == 2
# 2. 次に、要素の内容を確認する
assert User(name="Alice", is_active=True) in active_users
assert User(name="Charlie", is_active=True) in active_users
このテストでは、バグのある関数が3人のユーザーを返した時点でassert len(active_users) == 2
が失敗するため、即座に問題を検出できます。
解決策②(よりPython的):リスト全体を直接比較する
多くの場合、さらに簡潔で強力な方法があります。それは、期待される結果のリスト全体を、実際の戻り値と直接比較することです。
最も良い例:期待されるリストと完全一致するかを検証する
def test_get_active_users_with_full_list_comparison():
# Arrange
users = [
User(name="Alice", is_active=True),
User(name="Bob", is_active=False),
User(name="Charlie", is_active=True),
]
# 期待する結果の「完全な姿」を定義する
expected_users = [
User(name="Alice", is_active=True),
User(name="Charlie", is_active=True),
]
# Act
active_users = get_active_users(users)
# Assert: たった一行で、要素数・内容・順序のすべてを検証
assert active_users == expected_users
assert actual == expected
という一行のアサーションは、以下の3つの項目を同時に、かつ厳密に検証します。
- 要素数が一致しているか
- 各要素の内容が一致しているか
- 要素の順序が一致しているか
これにより、「期待しない要素が紛れ込む」ことも、「期待する要素が欠けている」ことも、「順序が狂っている」ことも、すべて一度に検出できます。戻り値の順序が重要でない場合は、assert sorted(actual) == sorted(expected)
のようにソートして比較するテクニックもあります。
まとめ
リストやタプルといったコレクションを返す関数をテストする際は、個々の要素の存在を確認するだけでは不十分です。
- 最低でも、
len()
を使って期待される要素数と一致するかを必ず検証する。 - 可能であれば、期待されるリスト全体と
==
で比較し、数・内容・順序の完全性を検証するのが最も堅牢な方法。
この一手間を惜しまないことで、テストの網羅性は格段に向上し、あなたのテストスイートはより信頼性の高い「品質の門番」として機能するようになります。