単体テスト仕様書の位置づけ

単体テスト仕様書は「各クラス・メソッドが単独で正しく動作することを検証するテストの設計書」だ。詳細設計のメソッド定義・業務ロジック定義を受けて、どのケースを検証するかを事前に定義する。

💡 テスト仕様書を先に書く

テスト仕様書を実装前に作成することで設計の曖昧さが明確になる。「このメソッドを NULL で呼んだら何を返す?」という問いに答えられない設計は不完全だ。

テスト対象スコープ

対象内容
テスト対象クラスOrderService, StockService, OrderValidator
テスト対象メソッド各クラスの public メソッド全て
テスト除外private メソッド(public 経由でテスト)、DTO(ゲッター/セッターのみ)
モック対象Repository(DB)、外部サービス呼び出し、メールサービス
カバレッジ目標行カバレッジ 80%以上、分岐カバレッジ 70%以上

テスト観点

観点確認内容
正常系正しい入力で期待値が返るか
境界値最小値・最大値・その前後で正しく動作するか
異常系(入力値)NULL・空文字・型違い・範囲外の値を渡したときの挙動
異常系(依存)依存するサービス・DBがエラーを返したときの挙動
業務ロジックコード値・フラグによる分岐が正しく動作するか

テストケース設計

テストID対象メソッド分類入力期待結果
UT-OS-001OrderService.confirmOrder正常系在庫あり商品・数量1注文IDが返却される
UT-OS-002OrderService.confirmOrder異常系在庫不足商品BizException(BIZ_001) がスローされる
UT-OS-003OrderService.confirmOrder境界値在庫数 = 注文数(ちょうど)注文確定成功
UT-OS-004OrderService.confirmOrder境界値在庫数 = 注文数 - 1BizException(BIZ_001) がスローされる
UT-OS-005OrderService.confirmOrder異常系userId = nullValueError がスローされる
UT-OS-006OrderService.cancelOrder正常系確認済み注文・本人操作キャンセル成功
UT-OS-007OrderService.cancelOrder異常系出荷済み注文BizException(BIZ_002) がスローされる

境界値・同値分割

境界値分析の例(数量バリデーション)
対象: OrderValidator.validateQuantity(quantity: int)
仕様: 数量は 1以上 9999以下の整数

同値クラス分割:
  有効クラス  : 1 ~ 9999
  無効クラスA : 0以下
  無効クラスB : 10000以上
  無効クラスC : NULL

境界値テストケース:
  ID      | 入力    | 期待
  --------|--------|---------------------
  BV-Q-01 | -1     | ValidationException
  BV-Q-02 | 0      | ValidationException(境界)
  BV-Q-03 | 1      | OK(境界)
  BV-Q-04 | 5000   | OK(代表値)
  BV-Q-05 | 9999   | OK(境界)
  BV-Q-06 | 10000  | ValidationException(境界)
  BV-Q-07 | NULL   | ValidationException

テスト仕様書の構成

セクション内容
1. テスト計画対象クラス・テスト環境・カバレッジ目標・実施期間
2. テスト観点一覧観点ごとの確認内容・優先度
3. テストケース一覧テストID・分類・入力・期待結果・実施結果
4. テストデータ使用するテストデータの定義・初期化方法
5. モック定義モック対象・スタブの戻り値
6. 実施結果合否・バグID・修正状況

Python Tips — pytest でのテスト実装

Python — pytest によるテスト仕様書の実装例
"""
pytest によるテスト仕様書の実装例
pip install pytest
"""
import pytest
from unittest.mock import MagicMock

class BizException(Exception):
    def __init__(self, code):
        self.code = code

class OrderService:
    def __init__(self, order_repo, stock_service):
        self.order_repo = order_repo
        self.stock_service = stock_service

    def confirm_order(self, user_id, product_id, quantity):
        if user_id is None:
            raise ValueError("user_id は必須です")
        available = self.stock_service.check_stock(product_id)
        if available < quantity:
            raise BizException("BIZ_001")
        order = self.order_repo.save(user_id, product_id, quantity)
        return order["order_id"]

class TestOrderServiceConfirmOrder:
    @pytest.fixture
    def service(self):
        repo = MagicMock()
        stock = MagicMock()
        repo.save.return_value = {"order_id": 12345}
        return OrderService(repo, stock), repo, stock

    def test_UT_OS_001_正常系(self, service):
        svc, repo, stock = service
        stock.check_stock.return_value = 10
        result = svc.confirm_order(user_id=1, product_id=100, quantity=1)
        assert result == 12345

    def test_UT_OS_002_在庫不足(self, service):
        svc, repo, stock = service
        stock.check_stock.return_value = 0
        with pytest.raises(BizException) as exc:
            svc.confirm_order(user_id=1, product_id=100, quantity=1)
        assert exc.value.code == "BIZ_001"

    @pytest.mark.parametrize("stock_qty,order_qty,ok", [
        (5, 5, True),   # UT-OS-003: 在庫 = 注文数
        (4, 5, False),  # UT-OS-004: 在庫不足
    ])
    def test_UT_OS_境界値(self, service, stock_qty, order_qty, ok):
        svc, repo, stock = service
        stock.check_stock.return_value = stock_qty
        if ok:
            assert svc.confirm_order(1, 100, order_qty) == 12345
        else:
            with pytest.raises(BizException):
                svc.confirm_order(1, 100, order_qty)

    def test_UT_OS_005_userId_null(self, service):
        svc, _, _ = service
        with pytest.raises(ValueError, match="user_id は必須です"):
            svc.confirm_order(user_id=None, product_id=100, quantity=1)

レビューチェックリスト

#チェック項目
1全 public メソッドにテストケースが設計されているか
2正常系・異常系・境界値がすべて網羅されているか
3NULL・空入力のケースが含まれているか
4依存クラスが適切にモック化されているか
5カバレッジ目標が定義されているか
6テストケースIDが設計書と対応しているか