Claude Code のSKILLでテスト自動化を実現:既存パターン学習とリファクタリングでカバレッジ向上
はじめに
テスト作成は重要ですが、時間がかかる作業です。特に以下のような課題があります:
- ⏱️ 時間がかかる: 1ファイルのテスト作成に30分〜1時間
- 🎯 試行錯誤が多い: モック戦略が定まらず、何度も書き直し
- 📐 一貫性がない: ファイルごとにテストの書き方が異なる
- 🤔 テスタビリティ判断が難しい: テストを書く前にリファクタリングすべきか判断できない
私たちが開発している「Delivroute」は、Google Maps APIを活用した配送ルート最適化SaaSです。最大10地点の配送ルートを自動で最適化し、配送効率を向上させることを目的としています。
このプロジェクトでは、Claude Code のカスタムスキルを3つ実装し、テスト自動化の課題を解決しました。
実績:
- 📈 カバレッジ 27% → 35% (Phase H完了時)
- 🧪 88テスト追加 (Phase H のみ)
- ⏱️ テスト作成時間 50%削減
- ✅ 重大バグ1件発見・修正
本記事では、テスト自動化を実現した3つのスキルの仕組みと連携方法を解説します。
3つのスキルの概要
テスト自動化を実現するために、以下の3つのスキルを実装しました:
1. create-test: 単一ファイルのテスト作成
役割: 単一ファイルの単体テスト作成に特化
特徴:
- 📚 既存テストパターンの自動学習: 同じディレクトリや外部依存のテストを自動参照
- 🎯 モック戦略の推論: localStorage, Date, fetch等のモック方法を既存パターンから推論
- 🧪 AAA パターンの自動適用: Arrange, Act, Assert の構造を自動適用
- 📊 カバレッジ目標 90%以上: 意味のあるテストを追加
2. test-expansion: 複数ファイルのテスト拡充
役割: 複数ファイルのテスト拡充を戦略的に管理
特徴:
- 📊 カバレッジレポート分析: 未テストファイルを自動抽出
- 🚦 テスタビリティ評価: 緑・黄・赤の3段階でテストしやすさを評価
- 🔄 create-test の呼び出し: 各ファイルに対して create-test を自動実行
- 📈 カバレッジ集計: 全体のカバレッジ向上を可視化
3. refactor-for-testability: リファクタリング
役割: テストしにくいコードをテストしやすい構造にリファクタリング
特徴:
- ⭐ Pure関数の抽出(最優先): 副作用なし、モック不要でテスト容易
- 🔗 疎結合化(依存性注入): 外部依存をモック可能に
- 🤖 create-test の自動呼び出し: リファクタリング完了後、自動的にテスト追加
- 📈 カバレッジ大幅向上: 20% → 85% の実績
スキルの2層構造
3つのスキルは以下のような関係になっています:
test-expansion(戦略層)
├── カバレッジレポート分析
├── テスタビリティ評価
│ ├── ✅ 緑(高)→ create-test 呼び出し
│ ├── ⚠️ 黄(中)→ ユーザー選択
│ └── ❌ 赤(低)→ refactor-for-testability 推奨
└── カバレッジ集計・報告
refactor-for-testability(リファクタリング層)
├── Pure関数の抽出
├── 疎結合化
└── create-test 自動呼び出し
create-test(実行層)
├── 既存テストパターン参照
├── テスト設計
├── テスト実装
└── テスト実行・検証
create-test スキル:単一ファイルのテスト作成
既存テストパターンの自動学習
create-test スキルの最大の特徴は、既存テストパターンを自動的に参照し、一貫性の高いテストを作成することです。
Phase 2: 既存テストパターンの参照
// 1. 同じディレクトリ内のテストファイルを検索
// 例: lib/cache/cache.ts の場合
// lib/cache/ 配下の *.test.ts ファイルを検索
// 2. 同じ外部依存を使っているテストを検索
// 例: localStorage を使う場合
grep -r "jest-localstorage-mock" lib --include="*.test.ts"
// 既存パターンから以下を学習:
// - describe/it の構造
// - beforeEach/afterEach の使い方
// - AAA パターンの実装
// - テストケースの命名規則(日本語 or 英語)
実装例:localStorage + Date.now() のモック
キャッシュ機能のテスト作成時に、以下のパターンを学習しました:
import 'jest-localstorage-mock'; // localStorage モック
// グローバルモックを解除(必要な場合)
jest.unmock('@/lib/cache');
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
import { CacheManager } from './cache';
let dateNowSpy: jest.SpyInstance;
beforeEach(() => {
// localStorage をクリア
localStorage.clear();
// 全モックをクリア
jest.clearAllMocks();
// console.error をモック化(エラーログ抑制)
jest.spyOn(console, 'error').mockImplementation(() => {});
// Date.now をモック化して時間を固定
dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(1000000);
});
afterEach(() => {
// 全モックをリストア
jest.restoreAllMocks();
});
describe('CacheManager', () => {
let cache: CacheManager;
beforeEach(() => {
cache = new CacheManager('app_cache');
});
describe('set', () => {
it('データを正しく保存できる', () => {
// Arrange: テストデータの準備
const key = 'user_123';
const data = { name: 'John Doe', email: 'john@example.com' };
const now = 1000000;
dateNowSpy.mockReturnValue(now);
// Act: テスト対象の実行
cache.set(key, data);
// Assert: 結果の検証
const stored = JSON.parse(localStorage.getItem('app_cache:user_123')!);
expect(stored.data).toEqual(data);
expect(stored.timestamp).toBe(now);
});
});
// 他のテストケース...
});
学習内容:
- ✅
jest-localstorage-mockをインポート - ✅
jest.unmock()でグローバルモックを解除 - ✅
Date.now()をモック化して時間を固定 - ✅
console.errorをモック化してログ抑制 - ✅ beforeEach/afterEach で状態リセット
- ✅ AAA パターン(Arrange, Act, Assert)遵守
- ✅ 日本語のテストケース名
実行結果
$ pnpm test cache.test.ts
PASS cache.test.ts
CacheManager
constructor
✓ デフォルトプレフィックスで初期化できる (3 ms)
✓ カスタムプレフィックスで初期化できる (1 ms)
get
✓ 保存されたデータを取得できる (2 ms)
✓ 期限切れのデータはnullを返す (1 ms)
✓ 不正なJSONデータはnullを返す (1 ms)
✓ 存在しないキーはnullを返す (1 ms)
set
✓ デフォルトTTLでデータを保存できる (2 ms)
✓ カスタムTTLでデータを保存できる (1 ms)
✓ LocalStorage満杯時にclearExpiredを呼び出す (3 ms)
remove
✓ データを削除できる (1 ms)
✓ 存在しないキーの削除はエラーにならない (1 ms)
clearExpired
✓ 期限切れのデータのみ削除される (2 ms)
✓ 他のプレフィックスのデータは削除されない (1 ms)
✓ パースエラーのデータは削除される (1 ms)
clearAll
✓ 全データを削除できる (1 ms)
✓ 他のプレフィックスのデータは削除されない (1 ms)
Test Suites: 1 passed, 1 total
Tests: 16 passed, 16 total
Snapshots: 0 total
Time: 0.5 s
----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
cache.ts | 82.75 | 72.72 | 85.71 | 82.75 |
----------|---------|----------|---------|---------|
成果:
- ✅ 16テストケース
- ✅ カバレッジ 82.75%
- ✅ 全テストパス
- ⏱️ 作成時間 15分(手動なら30-40分)
test-expansion スキル:複数ファイルのテスト拡充
テスタビリティ評価(緑・黄・赤)
test-expansion スキルの重要な機能が、テストを書く前にテスタビリティを評価することです。
評価基準
以下の5つの項目でテスタビリティを評価:
-
関数の長さ
- ✅ 50行未満 → そのままテスト可能
- ⚠️ 50-100行 → 関数分割を検討
- ❌ 100行以上 → リファクタリング推奨
-
外部依存の数
- ✅ 0-1つ → そのままテスト可能
- ⚠️ 2-3つ → 依存性注入を検討
- ❌ 4つ以上 → リファクタリング必須
-
副作用の複雑さ
- ✅ 副作用なし(Pure関数) → テスト容易
- ⚠️ 1種類の副作用のみ → モックで対応可能
- ❌ DB操作、API呼び出し、ファイルIOが混在 → Pure関数に分離
-
分岐の複雑さ(Cyclomatic Complexity)
- ✅ 分岐が4個以下 → そのままテスト可能
- ⚠️ 分岐が5-9個 → 分割を検討
- ❌ 分岐が10個以上 → 関数分割必須
-
ハードコードされた値
- ✅ ハードコードなし → そのままテスト可能
- ⚠️ 一部ハードコード → 改善余地あり
- ❌ API URL, タイムアウト値等がハードコード → 引数化・設定ファイル化
テスタビリティ判定
評価結果に基づいて、以下のように判定:
- A: テスタビリティ高(緑) → そのままテスト追加
- B: テスタビリティ中(黄) → ユーザー選択(簡単なリファクタリング or そのままテスト追加)
- C: テスタビリティ低(赤) → リファクタリング必須(refactor-for-testability 推奨)
実装例:lib/ 配下の3ファイル
以下のようなワークフローでテスト拡充を実施できます:
# ユーザー: lib/ 配下でカバレッジが低いファイルのテストを追加
$ /test-expansion lib/
# Phase 1: テスト対象の選定
# カバレッジレポート分析 → 未テストファイル5つ検出
# - lib/validation/rules.ts (0%)
# - lib/utils/formatter.ts (15%)
# - lib/session/handler.ts (0%)
# - lib/pricing/calculator.ts (20%)
# - lib/data/sorter.ts (10%)
#
# 優先度順にリストアップ → ユーザーに提示
# ユーザー選択 → 上位3ファイル
# Phase 2: テスタビリティ評価
# - lib/validation/rules.ts: ✅ テスタビリティ高(緑)
# - lib/utils/formatter.ts: ✅ テスタビリティ高(緑)
# - lib/session/handler.ts: ⚠️ テスタビリティ中(黄)
# → ユーザー選択: そのまま create-test 実行
# Phase 3: create-test 呼び出し
# `/create-test lib/validation/rules.ts` → 20/20 pass, 100%カバレッジ ✅
# `/create-test lib/utils/formatter.ts` → 15/15 pass, 92%カバレッジ ✅
# `/create-test lib/session/handler.ts` → 18/18 pass, 85%カバレッジ ✅
# Phase 4: カバレッジ集計
# 全体カバレッジ: 35.2% → 42.8% (+7.6%)
# 追加テスト数: 53テスト
# Phase 5: ドキュメント更新
# unit-test-strategy.md 更新
# 完了報告
成果:
- ✅ 53テストケース追加
- ✅ カバレッジ +7.6%
- ⏱️ 作成時間 1時間(手動なら3-4時間)
refactor-for-testability スキル:リファクタリング
Pure関数の抽出(最優先)
refactor-for-testability スキルの最重要原則は、Pure関数化を最優先することです。
Pure関数は以下の特徴があります:
- 🧪 同じ入力 → 同じ出力
- 🚫 副作用なし(API呼び出し、DB操作、ファイルIO等なし)
- ✅ モック不要でテスト可能
- 🔄 再利用性が高い
Before/After コード例
テストしにくいコードからテストしやすいコードへのリファクタリング例を紹介します。
Before: テストが難しいコード
async function processOrder(orderId: string) {
// データ取得(30行)
const order = await database.query('SELECT * FROM orders WHERE id = ?', [orderId]);
const user = await database.query('SELECT * FROM users WHERE id = ?', [order.userId]);
// バリデーション(20行)
if (!order) throw new Error('Order not found');
if (!user) throw new Error('User not found');
if (order.status !== 'pending') throw new Error('Order already processed');
// 価格計算(30行)
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
}
const tax = total * 0.1;
const finalPrice = total + tax;
// 決済(20行)
await paymentClient.charge({
amount: finalPrice,
currency: 'usd',
customerId: user.paymentCustomerId,
});
// 保存(10行)
await database.execute(
'UPDATE orders SET status = ?, total = ? WHERE id = ?',
['completed', finalPrice, orderId]
);
}
問題点:
- ❌ 関数の長さ: 110行
- ❌ 外部依存: 3つ(Database, PaymentClient, API)
- ❌ 副作用の混在: DB操作、API呼び出し、計算ロジックが混在
- ❌ Cyclomatic Complexity: 8
- ❌ テスタビリティ: 低(赤)
After: テストしやすいコード
// ✅ Pure関数(テスト容易、モック不要)
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
function calculateTax(total: number): number {
return total * 0.1;
}
function calculateFinalPrice(total: number, tax: number): number {
return total + tax;
}
// ✅ Pure関数(バリデーション)
function validateOrder(order: Order | null, user: User | null): void {
if (!order) throw new Error('Order not found');
if (!user) throw new Error('User not found');
if (order.status !== 'pending') throw new Error('Order already processed');
}
// ✅ 疎結合化(依存性注入)
interface OrderRepository {
getOrder(orderId: string): Promise<Order | null>;
updateOrder(orderId: string, data: Partial<Order>): Promise<void>;
}
interface UserRepository {
getUser(userId: string): Promise<User | null>;
}
interface PaymentService {
charge(customerId: string, amount: number): Promise<void>;
}
// メイン関数(統合)
async function processOrder(
orderId: string,
orderRepo: OrderRepository,
userRepo: UserRepository,
paymentService: PaymentService
) {
// データ取得
const order = await orderRepo.getOrder(orderId);
const user = await userRepo.getUser(order.userId);
// バリデーション(Pure関数)
validateOrder(order, user);
// 価格計算(Pure関数)
const total = calculateTotal(order.items);
const tax = calculateTax(total);
const finalPrice = calculateFinalPrice(total, tax);
// 決済
await paymentService.charge(user.paymentCustomerId, finalPrice);
// 保存
await orderRepo.updateOrder(orderId, { status: 'completed', total: finalPrice });
}
改善点:
- ✅ 関数の長さ: 各関数10-20行
- ✅ Pure関数の割合: 60%(calculateTotal, calculateTax, calculateFinalPrice, validateOrder)
- ✅ 疎結合化: 依存性注入でテスト時にモック注入可能
- ✅ Cyclomatic Complexity: 各関数1-3
- ✅ テスタビリティ: 高(緑)
create-test の自動呼び出し
リファクタリング完了後、自動的に create-test スキルを呼び出してテストを追加します。
describe('calculateTotal (Pure関数)', () => {
it('アイテムの合計金額を正しく計算できる', () => {
// Arrange
const items = [
{ price: 100, quantity: 2 },
{ price: 200, quantity: 1 },
];
// Act
const result = calculateTotal(items);
// Assert
expect(result).toBe(400); // 100*2 + 200*1 = 400
});
it('空配列の場合は0を返す', () => {
expect(calculateTotal([])).toBe(0);
});
});
describe('calculateTax (Pure関数)', () => {
it('税金を正しく計算できる', () => {
expect(calculateTax(1000)).toBe(100); // 1000 * 0.1 = 100
});
});
describe('validateOrder (Pure関数)', () => {
it('正常なorderとuserの場合はエラーをスローしない', () => {
const order = { status: 'pending', items: [], userId: '123' };
const user = { paymentCustomerId: 'cust_123' };
expect(() => validateOrder(order, user)).not.toThrow();
});
it('orderがnullの場合はエラーをスロー', () => {
expect(() => validateOrder(null, {})).toThrow('Order not found');
});
it('orderのstatusがpending以外の場合はエラーをスロー', () => {
const order = { status: 'completed', items: [], userId: '123' };
const user = { paymentCustomerId: 'cust_123' };
expect(() => validateOrder(order, user)).toThrow('Order already processed');
});
});
// メイン関数のテスト(モックを使用)
describe('processOrder', () => {
let mockOrderRepo: OrderRepository;
let mockUserRepo: UserRepository;
let mockPaymentService: PaymentService;
beforeEach(() => {
mockOrderRepo = {
getOrder: jest.fn(),
updateOrder: jest.fn(),
};
mockUserRepo = {
getUser: jest.fn(),
};
mockPaymentService = {
charge: jest.fn(),
};
});
it('正常なorderの場合は処理が完了する', async () => {
// Arrange
const order = {
userId: 'user_123',
status: 'pending',
items: [{ price: 100, quantity: 2 }],
};
const user = { paymentCustomerId: 'cust_123' };
(mockOrderRepo.getOrder as jest.Mock).mockResolvedValue(order);
(mockUserRepo.getUser as jest.Mock).mockResolvedValue(user);
// Act
await processOrder('order_123', mockOrderRepo, mockUserRepo, mockPaymentService);
// Assert
expect(mockPaymentService.charge).toHaveBeenCalledWith('cust_123', 220); // 200 + 20(税)
expect(mockOrderRepo.updateOrder).toHaveBeenCalledWith('order_123', {
status: 'completed',
total: 220,
});
});
});
成果:
- ✅ カバレッジ: 30% → 85% (+55%)
- ✅ Pure関数はモック不要で簡単にテスト可能
- ✅ 副作用部分のみモック使用(疎結合化のおかげ)
- ⏱️ リファクタリング + テスト作成: 1時間(手動なら3-4時間)
3スキル連携の実践例
test-expansion → refactor-for-testability → create-test
3つのスキルが連携して動作する実例を紹介します。
# ユーザー: lib/business-logic.ts のテストを追加
$ /test-expansion lib/business-logic.ts
# Phase 1: テスタビリティ評価
# - ファイル読み込み → 150行、複雑度15
# - 評価: ❌ テスタビリティ低(赤)
# - 関数の長さ: ❌ 150行(推奨: 50行未満)
# - 外部依存: ❌ 5つのAPI呼び出し(推奨: 1つ以下)
# - 分岐: ❌ 12個(推奨: 4個以下)
# - 推奨: リファクタリング必須
# Phase 2: ユーザー承認
# → リファクタリングを実施
# Phase 3: refactor-for-testability 実行
$ /refactor-for-testability lib/business-logic.ts
# - Phase 1: コード分析
# - Phase 2: リファクタリング戦略
# - Pure関数抽出: 3つの関数に分離
# - 疎結合化: 依存性注入でモック可能に
# - Phase 3: リファクタリング実装
# - Pure関数抽出完了 → 既存テスト確認 ✅
# - 疎結合化完了 → 既存テスト確認 ✅
# - Phase 4: create-test 自動呼び出し
# - カバレッジ: 20% → 85% ✅
# Phase 4: test-expansion に戻る
# - カバレッジ集計・報告
# - unit-test-strategy.md 更新
# 完了報告
# ✅ リファクタリング完了 + テスト追加完了
実際の効果(実プロジェクトの実績)
実際のプロジェクトでは、Business-Critical Tests フェーズで以下の成果を上げました:
対象ファイル:
-
lib/config/plans.ts(サービスプラン設定) -
lib/subscription/renewal.ts(サブスクリプション更新処理) -
lib/utils/date.ts(日付計算ユーティリティ)
実施内容:
-
config/plans.test.ts の作成
- 36テスト
- カバレッジ: 23.8% → 92.85% (+69.05%)
-
subscription/renewal.test.ts の作成
- 29テスト
- カバレッジ: 0% → 97.05% (+97.05%)
- 重大バグ発見・修正: 日付計算バグ(月末日 + 1ヶ月の計算が不正確)
-
utils/date.ts のリファクタリング・テスト作成
- 23テスト
- カバレッジ: 100%
- リファクタリング効果: renewal.ts の日付計算ロジック 19行 → 3行に簡略化
成果サマリー:
- ✅ 全体カバレッジ: 27.24% → 32.22% (+4.98%)
- ✅ テスト総数: 340 → 428 (+88テスト)
- ✅ lib/subscriptionカバレッジ: 38.93% → 98.21% (+59.28%)
- ✅ 重大バグ修正: 1件(サブスクリプション更新日計算)
- ✅ コード品質向上: Pure関数抽出、テスト容易性向上
スキルの作り方(Claude Code)
ディレクトリ構成
Claude Code のカスタムスキルは、以下のディレクトリ構成で管理します:
.claude/
├── skills/ # スキルディレクトリ
│ ├── create-test/
│ │ └── SKILL.md # スキル定義(615行)
│ ├── test-expansion/
│ │ └── SKILL.md # スキル定義(483行)
│ ├── refactor-for-testability/
│ │ └── SKILL.md # スキル定義(917行)
│ └── README.md # スキル一覧
└── commands/ # スラッシュコマンド
├── create-test.md
├── test-expansion.md
└── refactor-for-testability.md
SKILL.md の書き方
スキルファイル(SKILL.md)は、以下の構成で記述します:
---
name: create-test
description: 単一ファイルの単体テスト作成に特化したスキル - 既存テストパターンを自動学習し、一貫性の高いテストを効率的に作成
---
# create-test
## Instructions
単一ファイルの単体テスト作成に特化したスキルです。既存のテストパターンを自動的に参照し、モック戦略を学習することで、試行錯誤を削減し、一貫性の高いテストを効率的に作成します。
---
## 実行フロー
### Phase 1: 対象コードの分析
1. ファイルを読み込む
2. 依存関係の確認
3. テスト対象の分類
### Phase 2: 既存テストパターンの参照
1. 同ディレクトリ内のテストファイル検索
2. 同じ外部依存を使っているテストの検索
3. モック戦略を推論
### Phase 3: テスト設計
1. テストケース設計(正常系、異常系、境界値、エッジケース)
2. テストケース数の見積もり
3. モック戦略の決定
4. テスト計画書の作成 → ユーザー承認
### Phase 4: テスト実装
1. テストファイルの作成
2. モックの実装
3. AAA パターンの遵守
4. テストケースの命名規則
### Phase 5: テスト実行・検証
1. テスト実行
2. カバレッジ確認
3. ドキュメント更新
---
## 重要な原則
1. 既存パターンを最優先で参照
2. テスト設計を先に行う
3. カバレッジは手段であって目的ではない
4. モックは最小限に
5. テストは読みやすく
6. デバッグ時間を削減
---
## 実行例
(省略)
スラッシュコマンドの登録
スラッシュコマンド(.claude/commands/create-test.md)は、以下のように記述します:
`create-test` スキルを実行してください。
単一ファイルの単体テスト作成に特化したスキルです。既存のテストパターンを自動的に参照し、モック戦略を学習することで、試行錯誤を削減し、一貫性の高いテストを効率的に作成します。
**引数**: {{ARGUMENTS}}
これにより、以下のように実行できます:
/create-test lib/cache/storage.ts
まとめ
本記事では、Claude Code のカスタムスキルを3つ実装し、テスト自動化を実現した事例を紹介しました。
テスト自動化の効果
- ✅ テスト作成時間 50%削減: 既存パターン学習により試行錯誤が減少
- ✅ カバレッジ +5%向上: Pure関数化により効率的にカバレッジ向上
- ✅ 重大バグ発見: テスト作成中にビジネスロジックのバグを発見・修正
- ✅ テストの一貫性向上: モック戦略が統一され、メンテナンス性向上
3スキル連携の価値
- create-test: 既存パターン学習により、一貫性の高いテストを効率的に作成
- test-expansion: テスタビリティ評価により、無理にテストを書くのではなく、まずリファクタリング
- refactor-for-testability: Pure関数化 + 疎結合化により、テストしやすいコードに変換
Delivroute について
Delivrouteは、配送ルート最適化を簡単に行えるWebアプリケーションです。
- 🗺️ 最大10地点のルート最適化: Google Maps Routes APIで最適な配送順序を自動計算
- ⚡ 簡単CSVインポート: CSVファイルから一括で配送先を登録
- 💰 クレジット制: 無料プラン(月3回)、Proプラン(月60回)で気軽に利用可能
👉 サービスはこちら: https://delivroute.com
Discussion