JA Testing Testing Strategy - aku11i/phantom GitHub Wiki

テスト戦略

English | 日本語

このドキュメントでは、Phantomの包括的なテストアプローチと哲学について説明します。

テスト哲学

Phantomは以下のコアテスト原則に従います:

  1. 実装ではなく振る舞いをテスト - どのように動作するかではなく、何をするかに焦点を当てる
  2. 高速フィードバック - 頻繁な実行を促すため、テストは素早く実行されるべき
  3. 分離 - テストは外部状態や他のテストに依存してはならない
  4. 明確性 - テストの失敗は何が壊れているかを明確に示すべき
  5. 信頼性 - テストはコードの正確性に対する信頼を与えるべき

テストスタック

テストランナー

ソース: package.json#L15

PhantomはNode.jsの組み込みテストランナーを使用します:

node --test

利点:

  • 依存関係ゼロ
  • 高速実行
  • ネイティブTypeScriptサポート
  • 組み込みモッキング機能
  • 並列テスト実行

テストツール

  • Node.jsテストランナー - テスト実行とアサーション
  • モックモジュールAPI - ネイティブモジュールモッキング
  • TypeScript - 型安全なテストコード
  • 外部フレームワークなし - シンプルさと速度

テスト構成

ファイル構造

テストはソースファイルと同じ場所に配置されます:

src/
├── core/
│   ├── worktree/
│   │   ├── create.ts
│   │   ├── create.test.ts      # ユニットテスト
│   │   ├── delete.ts
│   │   └── delete.test.ts
│   ├── git/
│   │   ├── executor.ts
│   │   └── executor.test.ts
│   └── process/
│       ├── exec.ts
│       └── exec.test.ts

利点:

  • テストを見つけやすい
  • テストを促進
  • インポートを簡素化
  • 明確な関係性

テストファイルの命名

  • パターン: <module>.test.ts
  • 場所: ソースと同じディレクトリ
  • 実行: テストランナーによって自動的に発見

テストカテゴリー

1. ユニットテスト

目的: 個々の関数とモジュールを分離してテスト

特徴:

  • すべての外部依存関係をモック
  • 純粋関数を直接テスト
  • 高速実行(テストあたり< 10ms)
  • 高いコードカバレッジ

構造例:

import { describe, it, beforeEach, mock } from "node:test";
import assert from "node:assert";

describe("createWorktree", () => {
  beforeEach(() => {
    // モックをセットアップ
  });

  it("should create worktree with valid name", async () => {
    // テスト実装
  });

  it("should fail with invalid name", async () => {
    // エラーケースをテスト
  });
});

2. 統合テスト

目的: モジュール間の相互作用をテスト

特徴:

  • 限定的なモッキング(外部システムのみ)
  • モジュール境界をテスト
  • データフローを検証
  • 中程度の実行時間

現在の状態: 主にユニットテストで、一部統合的な側面あり

3. エンドツーエンドテスト

目的: 完全なユーザーワークフローをテスト

特徴:

  • モッキングなし
  • 実際のGit操作
  • 完全なコマンド実行
  • より遅い実行

現在の状態: 未実装(将来の検討事項)

モッキング戦略

モジュールモッキング

ソース: create.test.tsなどの各種テストファイル

PhantomはNode.jsのネイティブモジュールモッキングを使用します:

import { mock } from "node:test";

// ファイルシステムをモック
mock.module("node:fs/promises", {
  namedExports: {
    mkdir: mock.fn(() => Promise.resolve()),
    access: mock.fn(() => Promise.reject(new Error("Not found")))
  }
});

// 子プロセスをモック
mock.module("node:child_process", {
  namedExports: {
    spawn: mock.fn(() => mockProcess)
  }
});

モックパターン

1. Gitコマンドモッキング

const mockGitExecutor = {
  execute: mock.fn((args: string[]) => {
    if (args[0] === "worktree" && args[1] === "list") {
      return Result.ok("worktree /path/to/main\nHEAD abc123");
    }
    return Result.error(new Error("Command failed"));
  })
};

2. ファイルシステムモッキング

const mockFs = {
  access: mock.fn(async (path: string) => {
    if (path === "/tmp/phantom/repo") {
      throw new Error("ENOENT");
    }
  }),
  mkdir: mock.fn(async () => {})
};

3. プロセスモッキング

const mockProcess = {
  stdout: { on: mock.fn() },
  stderr: { on: mock.fn() },
  on: mock.fn((event, callback) => {
    if (event === "exit") {
      callback(0);
    }
  })
};

テストパターン

1. Result型テスト

成功と失敗の両方のパスをテスト:

it("should return ok result on success", async () => {
  const result = await createWorktree("feature");
  assert.strictEqual(result.ok, true);
  if (result.ok) {
    assert.strictEqual(result.value.name, "feature");
  }
});

it("should return error result on failure", async () => {
  const result = await createWorktree("../invalid");
  assert.strictEqual(result.ok, false);
  if (!result.ok) {
    assert.match(result.error.message, /invalid/i);
  }
});

2. エラーシナリオテスト

様々なエラー条件をテスト:

describe("error handling", () => {
  it("should handle git command failure", async () => {
    mockGit.execute.mockReturnValue(
      Result.error(new Error("Git failed"))
    );
    
    const result = await operation();
    assert.strictEqual(result.ok, false);
  });

  it("should handle file system errors", async () => {
    mockFs.access.mockRejectedValue(new Error("Permission denied"));
    
    const result = await operation();
    assert.strictEqual(result.ok, false);
  });
});

3. モック検証

正しい外部呼び出しを検証:

it("should call git with correct arguments", async () => {
  await createWorktree("feature", "main");
  
  assert.strictEqual(mockGit.execute.mock.calls.length, 1);
  assert.deepStrictEqual(
    mockGit.execute.mock.calls[0].arguments[0],
    ["worktree", "add", "--detach", "/tmp/phantom/repo/feature", "main"]
  );
});

テストデータ管理

テストフィクスチャ

一般的なテストデータパターン:

const fixtures = {
  validWorktree: {
    name: "feature-test",
    branch: "main",
    path: "/tmp/phantom/repo/feature-test"
  },
  
  gitWorktreeList: `worktree /path/to/main
HEAD abc123
branch refs/heads/main

worktree /tmp/phantom/repo/feature
HEAD def456
branch refs/heads/feature`,

  invalidNames: [
    "",
    ".",
    "..",
    "../escape",
    "with/slash",
    "with space"
  ]
};

テストヘルパー

再利用可能なテストユーティリティ:

function createMockGitExecutor(responses: Record<string, Result<string>>) {
  return {
    execute: mock.fn((args: string[]) => {
      const key = args.join(" ");
      return responses[key] || Result.error(new Error("Not mocked"));
    })
  };
}

function assertResultOk<T>(result: Result<T>): asserts result is { ok: true; value: T } {
  assert.strictEqual(result.ok, true, "Expected ok result");
}

カバレッジ戦略

カバレッジ目標

  • 目標: 80%以上のコードカバレッジ
  • 焦点: クリティカルパスとエラーハンドリング
  • 除外: シンプルなゲッター/セッター、型定義

カバレッジ測定

# カバレッジ付きでテストを実行
node --test --experimental-test-coverage

# カバレッジレポートを生成
node --test --experimental-test-coverage --test-reporter=lcov

カバレッジの優先順位

  1. 高優先度:

    • コアビジネスロジック
    • エラーハンドリングパス
    • データ変換
    • 検証ロジック
  2. 中優先度:

    • CLIハンドラー
    • 出力フォーマット
    • ヘルパーユーティリティ
  3. 低優先度:

    • シンプルなラッパー
    • 型定義
    • 定数

テスト実行

テストの実行

# すべてのテストを実行
pnpm test

# 特定のテストファイルを実行
node --test src/core/worktree/create.test.ts

# ウォッチモードで実行
node --test --watch

# カバレッジ付きで実行
node --test --experimental-test-coverage

CI/CD統合

ソース: GitHub Actionsワークフロー

テストは以下で実行されます:

  • すべてのプッシュ
  • すべてのプルリクエスト
  • 複数のNode.jsバージョン(v22、v24)
  • 複数のオペレーティングシステム

テスト品質ガイドライン

良いテストの特徴

  1. 独立性 - 他のテストへの依存なし
  2. 再現性 - 毎回同じ結果
  3. 自己検証 - 明確な合格/不合格
  4. タイムリー - コードと一緒に書かれる
  5. 焦点 - 1つのことをテスト

テスト命名規則

// 良い: 説明的な振る舞い
it("should create worktree with custom branch name")
it("should return error when name contains invalid characters")
it("should cleanup resources on failure")

// 悪い: 実装に焦点を当てている
it("should call git command")
it("should set property")
it("should work")

アサーションのベストプラクティス

// 良い: 特定のアサーション
assert.strictEqual(result.ok, true);
assert.strictEqual(worktree.name, "feature");
assert.match(error.message, /already exists/);

// 悪い: 汎用的なアサーション
assert(result.ok);
assert(worktree);
assert(error);

テストのデバッグ

テストの分離

// クリーンな状態のためにbeforeEachを使用
beforeEach(() => {
  mock.module("node:fs/promises", {
    namedExports: createMockFs()
  });
});

// テスト後のクリーンアップ
afterEach(() => {
  mock.reset();
});

デバッグ出力

// 一時的なデバッグロギング
it("should handle complex scenario", async () => {
  const result = await operation();
  
  // デバッグ出力
  console.log("Result:", JSON.stringify(result, null, 2));
  console.log("Mock calls:", mockGit.execute.mock.calls);
  
  assert.strictEqual(result.ok, true);
});

将来のテスト拡張

1. プロパティベーステスト

ランダムな有効/無効な入力を生成:

it("should handle any valid worktree name", () => {
  for (const name of generateValidNames()) {
    const result = validateWorktreeName(name);
    assert.strictEqual(result.ok, true);
  }
});

2. ミューテーションテスト

コードを変更してテスト品質を検証:

  • 演算子の変更
  • 定数の修正
  • 文の削除

3. パフォーマンステスト

クリティカルパスのベンチマークを追加:

it("should complete within performance budget", async () => {
  const start = performance.now();
  await operation();
  const duration = performance.now() - start;
  
  assert(duration < 100, `Operation took ${duration}ms`);
});

まとめ

Phantomのテスト戦略は以下を保証します:

  1. 信頼性 - 包括的なテストカバレッジ
  2. 保守性 - 明確で焦点を絞ったテスト
  3. 速度 - 高速なテスト実行
  4. 信頼性 - 徹底的なエラーテスト
  5. シンプルさ - ネイティブツール、複雑さなし

ネイティブNode.jsツーリングを使用した分離されたユニットテストに焦点を当てることで、堅牢で高速、保守可能なテストスイートが作成されます。

⚠️ **GitHub.com Fallback** ⚠️