Python | OOP:クラス単位のテスト

Python Python
スポンサーリンク

概要(クラス単位のテストは「1つの責務を独立して検証する」)

クラス単位のテストは、クラスの公開メソッドと外部との約束を中心に「入れたらこう返る」を独立して確かめる手法です。狙いは、仕様のズレや副作用のバグを早期に発見し、変更時にも安心してリファクタできる状態を保つこと。初心者は、Arrange–Act–Assert(準備→実行→検証)の型で書き、外部依存は注入してモックに差し替えることから始めましょう。


基本の流れ(Arrange–Act–Assertを型にする)

最小のテスト対象とテストコード(unittest)

# app/calculator.py
class Calculator:
    def add(self, a: float, b: float) -> float:
        return a + b

    def div(self, a: float, b: float) -> float:
        if b == 0:
            raise ZeroDivisionError("ゼロでは割れません")
        return a / b
Python
# tests/test_calculator_unittest.py
import unittest
from app.calculator import Calculator

class TestCalculator(unittest.TestCase):
    def setUp(self):
        # Arrange: 準備
        self.calc = Calculator()

    def test_add(self):
        # Act: 実行
        res = self.calc.add(2, 3)
        # Assert: 検証
        self.assertEqual(res, 5)

    def test_div_zero_raises(self):
        with self.assertRaises(ZeroDivisionError):
            self.calc.div(10, 0)

if __name__ == "__main__":
    unittest.main()
Python
  • 準備(インスタンス生成)→実行(メソッド呼び出し)→検証(戻り値や例外)。この型で「公開メソッドの契約」を明確にテストします。

pytestでより簡潔に書く

# tests/test_calculator_pytest.py
import pytest
from app.calculator import Calculator

def test_add():
    calc = Calculator()
    assert calc.add(2, 3) == 5

def test_div_zero_raises():
    calc = Calculator()
    with pytest.raises(ZeroDivisionError):
        calc.div(10, 0)
Python
  • pytestはアサートが素直に書け、失敗時の表示も読みやすいので初心者向けです。

依存を注入してモックする(外部I/Oを切り離す)

テストしやすい設計(依存性注入)

# app/user_service.py
class UserService:
    def __init__(self, repo, notifier):
        self.repo = repo
        self.notifier = notifier

    def register(self, name: str, email: str) -> dict:
        if not name.strip() or "@" not in email:
            raise ValueError("入力が不正")
        user = {"name": name.strip(), "email": email.strip()}
        saved = self.repo.save(user)   # 外部I/Oは注入した依存へ委譲
        self.notifier.welcome(saved["email"])
        return saved
Python
# tests/test_user_service.py
class StubRepo:
    def __init__(self): self._id = 0
    def save(self, user: dict) -> dict:
        self._id += 1
        return {**user, "id": str(self._id)}

class SpyNotifier:
    def __init__(self): self.sent = []
    def welcome(self, email: str):
        self.sent.append(email)

def test_register_success():
    svc = UserService(repo=StubRepo(), notifier=SpyNotifier())
    out = svc.register(" Taro ", " taro@example.com ")
    assert out["id"] == "1"
    assert out["name"] == "Taro"
    assert out["email"] == "taro@example.com"

def test_register_validation_error():
    svc = UserService(repo=StubRepo(), notifier=SpyNotifier())
    import pytest
    with pytest.raises(ValueError):
        svc.register("", "bad-email")
Python
  • 外部I/O(DBやメール送信)を“注入された依存”に逃がすことで、本体クラスのテストが純粋にできます。モック(Spy/Stub)で副作用の確認も容易です。

重要ポイントの深掘り(境界の選び方・パラメタ化・フィクスチャ)

公開メソッドに集中し、内部詳細はテストしない

クラスの「契約」に当たる公開メソッド(引数・戻り値・例外)を検証し、内部の私有メソッドや属性には依存しないのが原則です。内部は将来のリファクタリングで自由に変えられるべき領域だからです。

同型ケースをパラメタ化して網羅性を上げる(pytest)

import pytest
from app.calculator import Calculator

@pytest.mark.parametrize("a,b,expected", [
    (0, 0, 0),
    (2, 3, 5),
    (-1, 4, 3),
])
def test_add_param(a, b, expected):
    calc = Calculator()
    assert calc.add(a, b) == expected
Python
  • 同じロジックに対して複数の入力を簡潔にテストできます。境界値(0、負数、最大値)を意識して選ぶと効果的です。

フィクスチャで準備を共通化(pytest)

import pytest
from app.user_service import UserService

@pytest.fixture
def svc():
    class StubRepo:
        def __init__(self): self._id = 0
        def save(self, user): self._id += 1; return {**user, "id": str(self._id)}
    class SpyNotifier:
        def __init__(self): self.sent = []
        def welcome(self, email): self.sent.append(email)
    return UserService(StubRepo(), SpyNotifier())

def test_register_ok(svc):
    out = svc.register("Taro", "taro@example.com")
    assert out["id"] == "1"
Python
  • 毎回の準備コードを隠せるため、テストが短く読みやすく保てます。

耐久性を上げるテスト設計(副作用・時刻・乱数)

副作用は観測可能にし、外へ漏らさない

  • 書き込みや通知が起きたことは、Spy(記録するモック)で検証します。
  • ファイルやネットワークに直接触れないで、境界で抽象化(インターフェース)→モック差し替え。

時刻や乱数は固定する

# app/tokens.py
class TokenMaker:
    def __init__(self, rng, clock):
        self.rng = rng
        self.clock = clock
    def issue(self) -> dict:
        return {"token": str(self.rng()), "ts": self.clock()}

# tests/test_tokens.py
def test_issue_fixed():
    tm = TokenMaker(rng=lambda: 12345, clock=lambda: 1700000000)
    out = tm.issue()
    assert out["token"] == "12345"
    assert out["ts"] == 1700000000
Python
  • 乱数・現在時刻を注入して“固定値”化すると、テストが決定的になります(毎回同じ結果)。

よくある落とし穴(過度な内部依存・巨大テスト・順序依存)

内部実装に依存するアサート

私有属性の具体値や内部メソッドの呼び順に依存すると、リファクタで壊れやすくなります。公開契約(結果・例外)だけを検証し、必要ならテスト用の観測窓口(通知履歴など)を設計に用意します。

巨大な統合テストを“単体テスト”と呼ぶ

外部I/Oまで丸ごと通すと、失敗時の原因切り分けが難しく、遅く不安定です。ユニット(クラス)では依存をモックし、統合は別レイヤーのテストに分けます。

呼び出し順の固定に依存

順序が変わっても契約が満たされる設計が理想です。順序が意味を持つなら、仕様として明記し、その振る舞い(たとえば「検証に失敗したら保存しない」)をテストで確かめます。


例題(ドメインとインフラを分けたクラス単位テスト)

ドメイン計算は純粋にテスト

# app/price.py
class TaxRule:
    def __init__(self, rate: float): self.rate = rate
    def apply(self, amount: float) -> float:
        return round(amount * (1 + self.rate), 2)

def test_tax_rule():
    r = TaxRule(0.1)
    assert r.apply(100) == 110.00
    assert r.apply(0) == 0.00
Python
  • I/Oがない純粋ロジックは、シンプルなユニットテストで高速・堅牢。

インフラ依存は注入してモックで検証

# app/client.py
class ApiClient:
    def __init__(self, session, base_url: str):
        self.session = session; self.base_url = base_url
    def get_user(self, uid: str) -> dict:
        r = self.session.get(f"{self.base_url}/users/{uid}", timeout=5.0)
        r.raise_for_status()
        d = r.json()
        return {"id": str(d.get("id", "")), "name": d.get("name") or "unknown"}

# tests/test_client.py
class StubResp:
    def __init__(self, payload): self._p = payload
    def raise_for_status(self): pass
    def json(self): return self._p

class StubSession:
    def get(self, url: str, timeout: float):
        return StubResp({"id": 1, "name": "Taro"})

def test_get_user_ok():
    c = ApiClient(StubSession(), "https://api.example.com")
    assert c.get_user("1") == {"id": "1", "name": "Taro"}
Python
  • ネットワークを使わずに、成功・失敗の両パスを素早く検証できます。

まとめ(「契約中心」「依存は注入」「決定的に」を徹底する)

クラス単位のテストは、公開メソッドの契約(引数・戻り値・例外)に集中し、Arrange–Act–Assertで端的に書くことが肝心です。外部依存は注入してモックに差し替え、時刻・乱数は固定して決定的にする。pytestのパラメタ化やフィクスチャで網羅性と可読性を高め、ユニットと統合を分けて失敗の切り分けを楽にする。これを型として身につければ、初心者でも「速くて壊れにくい」テストを自然に書けるようになります。

タイトルとURLをコピーしました