JA Testing Testing Strategy - aku11i/phantom GitHub Wiki
English | 日本語
このドキュメントでは、Phantomの包括的なテストアプローチと哲学について説明します。
Phantomは以下のコアテスト原則に従います:
- 実装ではなく振る舞いをテスト - どのように動作するかではなく、何をするかに焦点を当てる
- 高速フィードバック - 頻繁な実行を促すため、テストは素早く実行されるべき
- 分離 - テストは外部状態や他のテストに依存してはならない
- 明確性 - テストの失敗は何が壊れているかを明確に示すべき
- 信頼性 - テストはコードの正確性に対する信頼を与えるべき
ソース: 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
- 場所: ソースと同じディレクトリ
- 実行: テストランナーによって自動的に発見
目的: 個々の関数とモジュールを分離してテスト
特徴:
- すべての外部依存関係をモック
- 純粋関数を直接テスト
- 高速実行(テストあたり< 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 () => {
// エラーケースをテスト
});
});
目的: モジュール間の相互作用をテスト
特徴:
- 限定的なモッキング(外部システムのみ)
- モジュール境界をテスト
- データフローを検証
- 中程度の実行時間
現在の状態: 主にユニットテストで、一部統合的な側面あり
目的: 完全なユーザーワークフローをテスト
特徴:
- モッキングなし
- 実際の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)
}
});
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"));
})
};
const mockFs = {
access: mock.fn(async (path: string) => {
if (path === "/tmp/phantom/repo") {
throw new Error("ENOENT");
}
}),
mkdir: mock.fn(async () => {})
};
const mockProcess = {
stdout: { on: mock.fn() },
stderr: { on: mock.fn() },
on: mock.fn((event, callback) => {
if (event === "exit") {
callback(0);
}
})
};
成功と失敗の両方のパスをテスト:
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);
}
});
様々なエラー条件をテスト:
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);
});
});
正しい外部呼び出しを検証:
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
-
高優先度:
- コアビジネスロジック
- エラーハンドリングパス
- データ変換
- 検証ロジック
-
中優先度:
- CLIハンドラー
- 出力フォーマット
- ヘルパーユーティリティ
-
低優先度:
- シンプルなラッパー
- 型定義
- 定数
# すべてのテストを実行
pnpm test
# 特定のテストファイルを実行
node --test src/core/worktree/create.test.ts
# ウォッチモードで実行
node --test --watch
# カバレッジ付きで実行
node --test --experimental-test-coverage
ソース: GitHub Actionsワークフロー
テストは以下で実行されます:
- すべてのプッシュ
- すべてのプルリクエスト
- 複数のNode.jsバージョン(v22、v24)
- 複数のオペレーティングシステム
- 独立性 - 他のテストへの依存なし
- 再現性 - 毎回同じ結果
- 自己検証 - 明確な合格/不合格
- タイムリー - コードと一緒に書かれる
- 焦点 - 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);
});
ランダムな有効/無効な入力を生成:
it("should handle any valid worktree name", () => {
for (const name of generateValidNames()) {
const result = validateWorktreeName(name);
assert.strictEqual(result.ok, true);
}
});
コードを変更してテスト品質を検証:
- 演算子の変更
- 定数の修正
- 文の削除
クリティカルパスのベンチマークを追加:
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のテスト戦略は以下を保証します:
- 信頼性 - 包括的なテストカバレッジ
- 保守性 - 明確で焦点を絞ったテスト
- 速度 - 高速なテスト実行
- 信頼性 - 徹底的なエラーテスト
- シンプルさ - ネイティブツール、複雑さなし
ネイティブNode.jsツーリングを使用した分離されたユニットテストに焦点を当てることで、堅牢で高速、保守可能なテストスイートが作成されます。