JA Testing Unit Testing - aku11i/phantom GitHub Wiki
ユニットテスト
English | 日本語
このガイドでは、Node.jsの組み込みテストランナーを使用したPhantomのユニットテストの書き方と実行方法について説明します。
テストフレームワーク
Node.jsテストランナー
PhantomはNode.jsのネイティブテストランナー(Node.js v22+で利用可能)を使用します:
import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert";
主な機能:
- Node.jsに組み込み(依存関係なし)
- TypeScriptサポート
- ネイティブモジュールモッキング
- Async/awaitサポート
- 並列実行
ユニットテストの書き方
基本的なテスト構造
import { describe, it } from "node:test";
import assert from "node:assert";
import { validateWorktreeName } from "./validate.js";
describe("validateWorktreeName", () => {
it("should accept valid names", () => {
const result = validateWorktreeName("feature-123");
assert.strictEqual(result.ok, true);
});
it("should reject empty names", () => {
const result = validateWorktreeName("");
assert.strictEqual(result.ok, false);
});
});
非同期関数のテスト
import { describe, it } from "node:test";
import assert from "node:assert";
describe("async operations", () => {
it("should handle async operations", async () => {
const result = await asyncOperation();
assert.strictEqual(result.ok, true);
});
it("should handle async errors", async () => {
const result = await failingAsyncOperation();
assert.strictEqual(result.ok, false);
if (!result.ok) {
assert.match(result.error.message, /expected error/);
}
});
});
フックの使用
describe("WorktreeManager", () => {
let manager: WorktreeManager;
let mockGit: MockGitExecutor;
beforeEach(() => {
mockGit = createMockGitExecutor();
manager = new WorktreeManager(mockGit);
});
afterEach(() => {
mock.reset();
});
it("should initialize correctly", () => {
assert(manager instanceof WorktreeManager);
});
});
モジュールモッキング
基本的なモジュールモック
ソース: src/core/worktree/create.test.ts
import { mock } from "node:test";
// fsモジュールをモック
mock.module("node:fs/promises", {
namedExports: {
access: mock.fn(() => Promise.reject(new Error("ENOENT"))),
mkdir: mock.fn(() => Promise.resolve()),
readFile: mock.fn(() => Promise.resolve("content"))
}
});
// カスタムモジュールをモック
mock.module("../git/executor.js", {
namedExports: {
GitExecutor: mock.fn(() => ({
execute: mock.fn(() => Result.ok("success"))
}))
}
});
高度なモッキングパターン
動的モックレスポンス
const mockGitExecute = mock.fn((args: string[]) => {
const command = args.join(" ");
switch (command) {
case "worktree list --porcelain":
return Result.ok("worktree /main\nHEAD abc123");
case "rev-parse --show-toplevel":
return Result.ok("/path/to/repo");
case "branch --show-current":
return Result.ok("main");
default:
return Result.error(new Error(`Unknown command: ${command}`));
}
});
mock.module("../git/executor.js", {
namedExports: {
GitExecutor: mock.fn(() => ({
execute: mockGitExecute
}))
}
});
状態を持つモッキング
describe("stateful operations", () => {
let callCount = 0;
const mockWithState = mock.fn(() => {
callCount++;
if (callCount === 1) {
return Result.error(new Error("First call fails"));
}
return Result.ok("Success on retry");
});
beforeEach(() => {
callCount = 0;
});
});
テストパターン
Result型のテスト
describe("Result type operations", () => {
it("should handle ok results", () => {
const result = functionReturningResult();
assert.strictEqual(result.ok, true);
if (result.ok) {
assert.strictEqual(result.value, expectedValue);
}
});
it("should handle error results", () => {
const result = functionThatFails();
assert.strictEqual(result.ok, false);
if (!result.ok) {
assert(result.error instanceof Error);
assert.match(result.error.message, /expected pattern/);
}
});
});
エラーシナリオのテスト
describe("error handling", () => {
it("should handle validation errors", async () => {
const result = await createWorktree("../invalid-name");
assert.strictEqual(result.ok, false);
if (!result.ok) {
assert.match(result.error.message, /invalid.*name/i);
}
});
it("should handle git command failures", async () => {
mockGit.execute.mockReturnValue(
Result.error(new Error("fatal: worktree already exists"))
);
const result = await createWorktree("duplicate");
assert.strictEqual(result.ok, false);
});
it("should handle file system errors", async () => {
mockFs.mkdir.mockRejectedValue(
new Error("EACCES: permission denied")
);
const result = await createWorktree("no-permission");
assert.strictEqual(result.ok, false);
});
});
コマンドハンドラーのテスト
ハンドラーテストの例:
describe("CreateHandler", () => {
let handler: CreateHandler;
let mockOutput: MockOutput;
beforeEach(() => {
mockOutput = createMockOutput();
handler = new CreateHandler(mockOutput);
});
it("should create worktree and display success", async () => {
await handler.execute({ name: "feature", branch: "main" });
// コア操作が呼ばれたことを確認
assert.strictEqual(mockCore.createWorktree.mock.calls.length, 1);
// 出力がフォーマットされたことを確認
assert.strictEqual(mockOutput.success.mock.calls.length, 1);
assert.match(
mockOutput.success.mock.calls[0].arguments[0],
/Created phantom 'feature'/
);
});
it("should display error on failure", async () => {
mockCore.createWorktree.mockReturnValue(
Result.error(new Error("Creation failed"))
);
await handler.execute({ name: "feature" });
assert.strictEqual(mockOutput.error.mock.calls.length, 1);
assert.strictEqual(process.exitCode, 1);
});
});
テストの編成
Describeブロック
階層的にテストを編成:
describe("Git Module", () => {
describe("GitExecutor", () => {
describe("execute method", () => {
it("should execute git commands", () => {
// テスト
});
it("should handle command errors", () => {
// テスト
});
});
describe("executeJson method", () => {
it("should parse JSON output", () => {
// テスト
});
});
});
});
テストの命名
説明的なテスト名を使用:
// 良いテスト名
it("should create worktree with specified branch")
it("should return error when worktree name contains path separator")
it("should cleanup resources when creation fails")
// 悪いテスト名
it("should work")
it("test create")
it("error case")
アサーション
一般的なアサーション
// 等価性アサーション
assert.strictEqual(actual, expected);
assert.deepStrictEqual(actualObject, expectedObject);
// ブール値アサーション
assert(condition);
assert.ok(value);
// エラーアサーション
assert.throws(() => {
throwingFunction();
}, /expected error message/);
// 非同期エラーアサーション
await assert.rejects(
async () => await failingAsyncFunction(),
{ message: /expected error/ }
);
// パターンマッチング
assert.match(string, /pattern/);
assert.doesNotMatch(string, /pattern/);
カスタムアサーション
// 型アサーションヘルパー
function assertOk<T>(
result: Result<T>
): asserts result is { ok: true; value: T } {
assert.strictEqual(result.ok, true, "Expected successful result");
}
// 使用方法
const result = await operation();
assertOk(result);
// TypeScriptはresult.valueが利用可能であることを知っている
console.log(result.value);
モック検証
モック呼び出しの検証
it("should call git with correct arguments", async () => {
await createWorktree("feature", "develop");
// 呼び出し回数を検証
assert.strictEqual(mockGit.execute.mock.calls.length, 2);
// 最初の呼び出しの引数を検証
assert.deepStrictEqual(
mockGit.execute.mock.calls[0].arguments,
["rev-parse", "--show-toplevel"](/aku11i/phantom/wiki/"rev-parse",-"--show-toplevel")
);
// 2番目の呼び出しの引数を検証
assert.deepStrictEqual(
mockGit.execute.mock.calls[1].arguments,
["worktree", "add", "--detach", "/tmp/phantom/repo/feature", "develop"](/aku11i/phantom/wiki/"worktree",-"add",-"--detach",-"/tmp/phantom/repo/feature",-"develop")
);
});
モック状態の検証
it("should not create directory if validation fails", async () => {
const result = await createWorktree("invalid/name");
// ファイルシステム操作に到達してはならない
assert.strictEqual(mockFs.mkdir.mock.calls.length, 0);
assert.strictEqual(mockGit.execute.mock.calls.length, 0);
});
テストデータとフィクスチャ
テストフィクスチャの作成
const testFixtures = {
worktrees: {
main: {
name: "main",
path: "/repo",
branch: "main",
isMain: true
},
feature: {
name: "feature",
path: "/tmp/phantom/repo/feature",
branch: "feature/new-ui",
isMain: false
}
},
gitOutput: {
worktreeList: `worktree /repo
HEAD abc123
branch refs/heads/main
worktree /tmp/phantom/repo/feature
HEAD def456
branch refs/heads/feature/new-ui`
},
invalidNames: ["", ".", "..", "../escape", "with/slash", "with space"]
};
テストビルダーの使用
class WorktreeBuilder {
private worktree: Partial<WorktreeInfo> = {};
withName(name: string): this {
this.worktree.name = name;
return this;
}
withBranch(branch: string): this {
this.worktree.branch = branch;
return this;
}
build(): WorktreeInfo {
return {
name: this.worktree.name || "test",
branch: this.worktree.branch || "main",
path: `/tmp/phantom/repo/${this.worktree.name || "test"}`,
isMain: false
};
}
}
// テストでの使用
const worktree = new WorktreeBuilder()
.withName("feature")
.withBranch("develop")
.build();
テストの実行
コマンドライン
# すべてのテストを実行
pnpm test
# 特定のテストファイルを実行
node --test src/core/worktree/create.test.ts
# パターンに一致するテストを実行
node --test --test-name-pattern="validation"
# ウォッチモードで実行
node --test --watch
# カバレッジ付きで実行
node --test --experimental-test-coverage
IDE統合
VS Code
.vscode/launch.json
に追加:
{
"type": "node",
"request": "launch",
"name": "Run Current Test File",
"program": "${file}",
"args": ["--test"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
}
ベストプラクティス
1. テストの独立性
各テストは独立している必要があります:
// 良い: 独立したテスト
it("should create worktree", async () => {
const result = await createWorktree("feature");
assert.strictEqual(result.ok, true);
});
// 悪い: 前のテストに依存
it("should list created worktree", async () => {
// 前のテストが"feature"を作成したと仮定
const result = await listWorktrees();
assert(result.value.some(w => w.name === "feature"));
});
2. 明確なテスト構造
Arrange-Act-Assertパターンに従う:
it("should handle missing branch", async () => {
// Arrange
mockGit.execute.mockImplementation((args) => {
if (args.includes("rev-parse")) {
return Result.error(new Error("unknown revision"));
}
return Result.ok("");
});
// Act
const result = await createWorktree("feature", "non-existent");
// Assert
assert.strictEqual(result.ok, false);
assert.match(result.error.message, /branch.*not.*found/i);
});
3. 意味のあるテストデータ
現実的なテストデータを使用:
// 良い: 現実的なデータ
const testBranch = "feature/user-authentication";
const testPath = "/home/user/projects/myapp";
// 悪い: 意味のないデータ
const testBranch = "foo";
const testPath = "/test";
4. エラーメッセージのテスト
デバッグのためにエラーメッセージをテスト:
it("should provide helpful error message", async () => {
const result = await createWorktree("my/feature");
assert.strictEqual(result.ok, false);
if (!result.ok) {
// エラーメッセージがユーザーに役立つことを確認
assert.match(result.error.message, /cannot contain/);
assert.match(result.error.message, /slash/);
}
});
まとめ
Phantomのユニットテスト:
- ネイティブツール - Node.js組み込みテストランナーを使用
- すべてをモック - テスト対象を分離
- 振る舞いをテスト - 実装ではなく結果に焦点を当てる
- 明確な構造 - 整理された読みやすいテスト
- 高速実行 - テストあたりミリ秒
これらのパターンに従うことで、コードの正確性に対する信頼を与える、信頼性が高く保守可能なテストが保証されます。