Programming
Quality

テスト駆動開発(TDD)

Red → Green → Refactor の短サイクルで“変更に強い”設計を生む。小さく、速く、安全に。

RED GREEN REFACTOR Outside-in xUnit Test Double Property-based

概要

テスト駆動開発(Test-Driven Development)は、テストを先に書き、そのテストを通す最小実装を行い、リファクタリングで設計を洗練していく開発手法です。

  • 変更容易性:小さなサイクルが回帰を可視化し、安心して改善できる
  • 設計の質:テスト可能設計(疎結合・明確な責務)を自然に促す
  • ドキュメント性:テストが実行可能な仕様として残る

基本 / 基礎

テストの書き方(xUnit共通)

  • Arrange(前提)→ Act(実行)→ Assert(検証)
  • 1テスト1アサーションに近づけ、意図が読める名前にする
  • 外部I/Oはテストダブルで置き換え、純粋なロジックを中心に

テストダブル

  • Dummy / Stub / Spy / Mock / Fake の役割を明確化
  • 振る舞い検証(Mock)より状態検証を優先

小さな設計の技法

  • 副作用を分離(関数コア+命令型シェル)
  • 依存の注入(DI)でテスト可能性を確保
  • 境界づけられたコンテキストを意識

失敗から始める理由

  • 失敗を見ずにGreenにすると、テストが無効かもしれない(偽陽性)
  • 過剰実装を防ぎ、最小限のAPIを導く

応用 / 発展

Outside-in TDD

  • ユーザ要求 → 薄いエンドポイントから外部境界の契約を先に固定
  • 中に向かって責務を分解し、小さな単体テストで埋める

プロパティベーステスト

  • 例ではなく性質(不変条件)で仕様を記述
  • 境界値や乱数生成でケース網羅性を高める

ミューテーションテスト

  • コードを自動で変異させ、テストが殺せるかを評価
  • 単純なカバレッジより実効的品質を測る

契約テスト / コンシューマ駆動

  • マイクロサービス間では契約テスト(例:Pact)で整合性を守る
  • UIはビジュアルリグレッションで差分検出

サンプル(Swift / Java / JavaScript)


// Calculator.swift
struct Calculator {
    func add(_ a: Int, _ b: Int) -> Int { a + b }
    func isLeapYear(_ y: Int) -> Bool {
        // 400の倍数は閏年、100の倍数は平年、4の倍数は閏年
        if y % 400 == 0 { return true }
        if y % 100 == 0 { return false }
        return y % 4 == 0
    }
}

// CalculatorTests.swift
import XCTest
@testable import AppModule

final class CalculatorTests: XCTestCase {
    func testAdd() {
        XCTAssertEqual(Calculator().add(2, 3), 5)
    }
    func testLeapYearRules() {
        XCTAssertTrue(Calculator().isLeapYear(2024))
        XCTAssertFalse(Calculator().isLeapYear(1900))
        XCTAssertTrue(Calculator().isLeapYear(2000))
    }
}

ブラウザ内ミニTDDプレイグラウンド DEMO

下の関数を編集して を押すと、即席のユニットテストが走ります(安全のため、new Function で限定的に評価)。

Waiting

仕事の現場(導入チェックリスト)

準備

  • CIでテストを自動実行(PR/merge時)
  • テストフレームワーク・モックツールを整備
  • 速い単体テストと遅いE2Eを分離・タグ付け
  • テストの命名規約と階層(unit / integration / e2e)

運用

  • 新規機能はまず失敗テストから
  • バグは再現テストを先に追加
  • ミューテーションテスト/契約テストで品質の穴を把握
  • カバレッジは指標であり目標ではない(盲目的100%を避ける)

Web開発 / アプリ開発

Web開発

推奨ポイント
ドメイン単体(Jest/Vitest, JUnit)副作用隔離・純粋ロジック化
API統合(Contract, Supertest)契約テストで依存切断
UIUIテスト(RTL, Playwright)振る舞い中心・過度なスナップショット避け

アプリ開発

レイヤiOSAndroid
ユニットXCTestJUnit / KotlinTest
UIXCUITestEspresso
DIFactory/ProtocolHilt/Koin

知っておくといい(知識・技能・用語)

知識

  • SOLID / Clean Architecture
  • テストピラミッド / コーン
  • 副作用と純粋関数、境界とコア

技能

  • テストファーストでAPIを設計する
  • テストダブルの適切な選択
  • 小さなリファクタリングの連続

用語

  • AAA(Arrange-Act-Assert)
  • Given-When-Then(BDD)
  • Mutation / Property-based / Contract

例(小さなドメイン)


// 例:BankAccount のTDD(擬似コード)
class BankAccount {
  constructor(){ this.balance = 0 }
  deposit(amount){ if(amount<=0) throw new Error('invalid'); this.balance += amount }
  withdraw(amount){ if(amount<=0 || amount>this.balance) throw new Error('invalid'); this.balance -= amount }
}

test('deposit increases balance', () => {
  const a = new BankAccount()
  a.deposit(100)
  assert.equal(a.balance, 100)
})

test('cannot overdraw', () => {
  const a = new BankAccount()
  a.deposit(50)
  assert.throws(() => a.withdraw(100))
})