「この関数のテストコード、どう書けばいいんだ…?」 開発中にそう頭を抱えた経験はありませんか?多くの場合、その原因はテストの書き方の問題ではなく、テスト対象のコードの設計そのものにあります。
「テストのしにくさ」は、コードが抱える設計上の問題点を映し出す鏡です。一つの関数に責務が集中しすぎている、外部への依存が強すぎるなど、様々な「コードの匂い」を教えてくれます。
今回は、テストが困難な巨大関数を例に、テストのしやすさを追求することが、いかにしてクリーンで堅牢なクラス設計に繋がるかを、具体的なリファクタリングを通して見ていきましょう。
問題点:テストが困難な、巨大で多機能な関数
あるCSVファイルから学生の成績データを読み込み、無効なデータを除外しつつ、全体の平均点を算出する関数を考えてみましょう。
悪い例:すべてのロジックが一つの関数に詰め込まれている
# reports/analyzer.py
import csv
def process_grade_report(filepath: str) -> tuple[float, list]:
    """成績CSVを読み込み、有効なデータを集計して平均点とリストを返す。"""
    valid_grades = []
    with open(filepath, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            # 1. パースと型変換
            try:
                row["student_id"] = int(row["student_id"])
                row["score"] = int(row["score"])
            except (ValueError, KeyError):
                continue # 型変換できない行はスキップ
            # 2. バリデーション
            if not (0 <= row["score"] <= 100):
                continue # 0点未満、100点超の行はスキップ
            
            valid_grades.append(row)
    # 3. 集計
    if not valid_grades:
        return 0.0, []
        
    total_score = sum(g["score"] for g in valid_grades)
    average = total_score / len(valid_grades)
    return average, valid_grades
このprocess_grade_report関数は、ファイルI/O、データパース、バリデーション、集計という、少なくとも4つの異なる責務を抱えています。この関数をテストしようとすると、次のような困難に直面します。
- テストの準備が煩雑: 「スコアがマイナス点」という一つの異常系をテストしたいだけなのに、毎回CSVファイルをテストコード内で作成する必要があります。
 - 関心の分離ができていない: バリデーションロジックのテストと、平均点の計算ロジックのテストを独立して行うことができません。
 - テストが遅くなる: ファイルI/Oを伴うため、テストの実行速度が遅くなります。
 
解決策:テストのしやすさを道しるべに、責務をクラスに分割する
この困難を解消する鍵は、「どうすれば、もっと楽にテストできるか?」と自問し、コードをより小さな、独立した単位に分割していくことです。
ステップ1:単一のデータ構造を表現するクラスを作る
まず、責務の最小単位である「一人の学生の一つの成績」を表現するGradeクラスを定義します。そして、それに関するバリデーションロジックをクラスの責務として持たせます。
reports/models.py
from dataclasses import dataclass
@dataclass
class Grade:
    student_id: int
    score: int
        
    def validate(self):
        """この成績データが有効であるかを検証する。"""
        if not (0 <= self.score <= 100):
            raise ValueError("スコアは0から100の間でなければなりません。")
Gradeクラスのテスト (tests/test_models.py): これで、バリデーションロジックをファイルI/Oから完全に切り離し、メモリ上で高速にテストできるようになりました。
import pytest
from reports.models import Grade
def test_grade_validation_raises_error_when_score_is_negative():
    invalid_grade = Grade(student_id=1, score=-10)
    with pytest.raises(ValueError):
        invalid_grade.validate()
ステップ2:データの集合を扱うクラスを作る
次に、「成績データの集合(レポート)」を表現するGradeReportクラスを定義します。CSVファイルからデータを読み込む責務や、全体の平均点を計算する責務をこのクラスに移動させます。
reports/models.py
import csv
from dataclasses import dataclass
from typing import Iterator, List
# (Gradeクラスの定義は省略)
@dataclass
class GradeReport:
    grades: List[Grade]
        
    @property
    def average_score(self) -> float:
        """レポート全体の平均点を計算する。"""
        if not self.grades:
            return 0.0
        total_score = sum(g.score for g in self.grades)
        return total_score / len(self.grades)
        
    @classmethod
    def from_csv(cls, filepath: str) -> "GradeReport":
        """CSVファイルから有効な成績データを読み込み、インスタンスを生成する。"""
        valid_grades = []
        with open(filepath, "r", encoding="utf-8") as f:
            for row in csv.DictReader(f):
                try:
                    grade = Grade(
                        student_id=int(row["student_id"]),
                        score=int(row["score"])
                    )
                    grade.validate() # ここで単一データのバリデーションを呼び出す
                    valid_grades.append(grade)
                except (ValueError, KeyError):
                    continue # 無効な行はスキップ
        return cls(grades=valid_grades)
GradeReportクラスのテスト (tests/test_models.py): 平均点の計算ロジックも、ファイルI/Oとは無関係に、Gradeオブジェクトのリストを直接渡すことでテストできます。
def test_grade_report_average_score():
    report = GradeReport(grades=[
        Grade(student_id=1, score=80),
        Grade(student_id=2, score=90),
        Grade(student_id=3, score=100),
    ])
    assert report.average_score == 90.0
結果:テスト容易性の高い、クリーンな設計
リファクタリングの結果、巨大な関数は、それぞれが単一の責務を持つ2つのクラスに分割されました。
Gradeクラス: 一つの成績データの構造と、それ自体の正当性を保証する責務を持つ。GradeReportクラス:Gradeの集合を管理し、ファイルからの読み込みや、集合全体に対する集計処理の責務を持つ。
この設計は、**単一責任の原則(Single Responsibility Principle)**に従っており、各クラスが独立してテスト可能であるため、非常に堅牢で保守性の高いものになりました。
まとめ
コードのテストが難しいと感じたときは、一度立ち止まって、そのコードの設計を疑ってみましょう。
- テストのしにくさは、責務が過剰であることのサイン。
 - 「どうすればテストしやすくなるか?」という問いは、責務を分離し、関心を分離するための強力な羅針盤となる。
 
ユニットテストを書くという行為は、単なる品質保証活動ではありません。それは、コードの設計を健全な方向へと導くための、最も効果的なフィードバックループの一つなのです。
