Cursor Commands活用術 ― 単体テスト自動生成プロンプト

Cursor Commands活用術:単体テスト自動生成プロンプト

はじめに

「テストは大事だ」と頭では分かっていても、実際のプロジェクトでは工数の都合でなかなか書けない──そんな状況になったことはないでしょうか。

私自身もまさにその1人で、「時間がないから」「優先度が…」という理由でテストを書かないまま進めてしまうことがありました。

そんな悩みを抱えていた私ですが、単体テストを自動生成するCursorのCommandsを作成することで悩みを解消することに成功しました。

本記事ではテストに関する悩み、そしてその悩みを解消するために作成したCommandsについて紹介していきたいと思います。

テストを書くべきなんだけど…

まず大前提として、テストは本来きちんと書くべきものです。
書かないまま進めてしまうと、以下のようなデメリットがあります。

  • 手動確認に時間がかかる
    • テストがないため、毎回手動で画面操作やAPI実行を行う必要がある。
    • 自動実行による確認ができず、作業効率が低下する。
  • コードレビューに余計な時間がかかる
    • 仕様や想定していた使い方がテストで表現されていないため、PRに詳細な説明を書く必要がある。
    • レビュアー側も動作確認しながら仕様を理解しなければならず、負担が増える。
  • マージ後の手戻りが発生しやすい
    • テストで網羅的なケースを確認できないため、見落としが発生しやすい。
    • QAやリリース後に問題が発覚した場合、調査・修正対応に時間を取られる。
  • テストしづらいコードに手を加える必要がある
    • 後からテストを書くと、テスト容易性(テスタビリティ)を高めるために、依存関係の分離や関数の分割といったコード構造の修正が必要になる。
    • 再テストや影響調査など、余計な作業が必要になる。
  • リファクタリングが困難
    • 安全にリファクタリングできず、作業時間やリスクが増加する。
    • リファクタリングを避けると、技術的負債が蓄積される。

…とはいえ、実務では「時間がない」という言い訳のもと、書かれないこともしばしば。
冒頭に書いた通り、私もそんな言い訳をしてきた1人です。

それでも書くべきだ!という思いはあったので、 単体テストを自動生成するCommandsを作成することでテスト作成の工数削減に挑戦しました。

Commandsとは

CursorにはCommands機能というものが存在します。

以下の画像のように、チャット入力欄で「/」を付けるだけで呼び出せる再利用可能なワークフローを作成する機能で、公式ではコードレビューやプルリクエストの作成といった例が紹介されています。

Commands入力だけではなく、文を追加することもできます。

詳しくは公式ドキュメントをご覧ください。

準備:まずはCommandsを使ってみる

Commandsを作ったことはなかったのでどうしようかと考えていたところ、Xでテスト生成に使えそうなポストを見かけました。

そこでポスト内のプロンプトを環境だけ変えてそのまま使用し、自分なりにカスタマイズすることにしました。

# テストコード生成用の基本プロンプト(汎用版)


```
【環境】
- 言語: TypeScript
- テストフレームワーク: jest


【必須要件】
1. まず「テスト観点の表(等価分割・境界値)」をMarkdown表で提示
2. その表に基づいてテストコードを実装
3. 失敗系を正常系と同数以上含める
4. 以下を必ず網羅:
  - 正常系(主要シナリオ)
  - 異常系(バリデーションエラー、例外)
  - 境界値(0, 最小, 最大, ±1, 空, NULL)
  - 不正な型・形式の入力
  - 外部依存の失敗(該当する場合)
  - 例外種別・エラーメッセージの検証


5. 各テストケースにGiven/When/Then形式のコメント付き
6. 実行コマンドとカバレッジ取得方法を末尾に記載
7. 目標: 分岐網羅100%


不足している観点があれば自己追加してから実装してください。
```


# 使い方


1. エディターでテスト対象のコード(関数/クラス/モジュール)をコンテキストに入れる
2. 上記プロンプトをコピー
3. `{例: Python}` `{例: pytest}` の部分を実際の環境に置き換え
4. AI に送信

すると大きな問題点が2つ見えてきました。

  1.  明らかに必要ない失敗系を生成してくる
  2.  同じ条件のテストを生成してくる

これらは必須要件にある「失敗系を正常系と同数以上含める」と「正常系、異常系、境界値…を必ず網羅」という条件によって、明らかに必要ない失敗系や他のテストに条件を包含されたテストが生成されてしまうようです。

他にも細かい修正点はありますが、上記の2つを解消すれば叩き台としては十分なテストを生成してくれるという結果が見えました。

実践:Commandsを作る

問題点がわかったところでCommandsを作っていきます。

しかし全て手書きで、というのも手間がかかりますし他にも見えない改善点があるかもしれません。

そこでMulti Agent機能を使用して、先ほど挙げた2つの問題点を改善したCommandsを複数のモデルに提案してもらいました。

複数モデルの提案をいいとこ取りしつつ、細かい修正を加え、以下の単体テスト生成Commandsが完成しました!

# テスト作成


【環境】


- 言語: TypeScript
- テストフレームワーク: jest


【必須要件】


1. まず「テスト観点の表(等価分割・境界値)」を Markdown 表で提示
2. その表に基づいてテストコードを実装
3. 失敗系と正常系のどちらも作成する
4. 以下を必ず網羅:
  - 正常系(主要シナリオ)
  - 異常系(バリデーションエラー、例外)
  - 境界値(0, 最小, 最大, ±1, 空, NULL)
  - 不正な型・形式の入力
  - 外部依存の失敗(該当する場合)
  - 例外種別・エラーメッセージの検証
5. 各テストケースに Given/When/Then 形式のコメント付き
6. 目標: 分岐網羅 100%


不足している観点があれば自己追加してから実装してください。


【テストケースの分類基準】


- **正常系**: 一般的な使用ケースで、期待される動作を検証する
- **異常系**: エラーや例外が発生するケースを検証する
- **境界値**: 正常系や異常系ではカバーできない、特殊な値や状態を検証する
 - 例: 最小値・最大値、±1 の値、空値、NULL、極端な値など
 - **注意**: 正常系や異常系で既にカバーされているケースは境界値として追加しない


【テストの実装先】


- `/src/utils/__test__`に実装すること
- テストファイル名はテストを書く関数のファイル名に「.test.ts」をつけること
 - 例:index.ts 内の関数のテストを実装 -> `/src/utils/__tests__/index.test.ts`に実装
- すでにファイルがある場合はそのファイルの中に、ない場合はファイルを作成


【実装前の最終確認】


テストコードを実装する前に、以下を確認してください:


1. テスト観点表に重複がないか
2. 各テストケースが異なる観点を検証しているか
3. 境界値が正常系や異常系で既にカバーされていないか
4. すべてのテストケースが「追加価値がある」か
5. テストを実装した関数に欠陥がないか
  - 関数が異常値や境界値を考えた実装になっておらず、クラッシュなどの危険性がある場合は警告を出すこと


【実装後の最終確認】


テストコードを実装した後に、以下を確認してください:


1. テスト観点表と実装に差異がないか
  - テスト観点表に記載されたすべてのケースが実装されているか
  - テスト観点表に記載されていないテストケースが追加されていないか(意図しない追加がないか)
  - 表の行数とテストケース数が一致しているか
2. すべてのテストケースが実際に実行されているか(`npm test`で実行してエラーがないか)
3. 各テストケースに Given/When/Then 形式のコメントが適切に記載されているか
4. テスト観点表の分類(正常系/異常系/境界値)が実装のコメントや describe/it の説明と一致しているか
5. モックやスタブが適切に使用されているか(外部依存が正しくモックされているか)
6. テストの独立性が保たれているか(beforeEach/afterEach で適切にクリーンアップされているか)
7. エラーメッセージやアサーションが適切か(エラー時に原因が特定しやすいか)
8. テストコードの可読性(変数名、テスト名が意図を明確に表現しているか)

実際にCommandsを叩いて出力されたテストの一部が以下です。

// ============================================
// テスト観点表: isDateInRange
// ============================================
// | 分類 | テストケース | 入力 | 期待値 |
// |------|------------|------|--------|
// | 正常系 | 範囲内の日付 | '2025-01-01', '2025-12-31', '2025-06-15' | true |
// | 正常系 | 開始日と同じ日付 | '2025-01-01', '2025-12-31', '2025-01-01' | true |
// | 正常系 | 終了日と同じ日付 | '2025-01-01', '2025-12-31', '2025-12-31' | true |
// | 正常系 | 範囲外(開始日より前) | '2025-01-01', '2025-12-31', '2024-12-31' | false |
// | 正常系 | 範囲外(終了日より後) | '2025-01-01', '2025-12-31', '2026-01-01' | false |
// | 正常系 | デフォルトで現在日時を使用 | '2025-01-01', '2025-12-31' | 現在日時に応じた結果 |
// | 境界値 | 開始日と終了日が同じ | '2025-01-01', '2025-01-01', '2025-01-01' | true |
// | 異常系 | 無効な日付文字列(startDate) | 'invalid-date', '2025-12-31', '2025-06-15' | false(Invalid Dateの比較結果) |
// | 異常系 | 無効な日付文字列(endDate) | '2025-01-01', 'invalid-date', '2025-06-15' | false(Invalid Dateの比較結果) |
// | 異常系 | 空文字列(startDate) | '', '2025-12-31', '2025-06-15' | false(Invalid Dateの比較結果) |
// | 異常系 | 空文字列(endDate) | '2025-01-01', '', '2025-06-15' | false(Invalid Dateの比較結果) |


describe('isDateInRange', () => {
 it('範囲内の日付の場合はtrueを返す', () => {
   // Given: 範囲内の日付が与えられる
   const startDate = '2025-01-01';
   const endDate = '2025-12-31';
   const targetDate = new Date('2025-06-15');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: trueが返される
   expect(result).toBe(true);
 });


 it('開始日と同じ日付の場合はtrueを返す', () => {
   // Given: 開始日と同じ日付が与えられる
   const startDate = '2025-01-01';
   const endDate = '2025-12-31';
   const targetDate = new Date('2025-01-01');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: trueが返される
   expect(result).toBe(true);
 });


 it('終了日と同じ日付の場合はtrueを返す', () => {
   // Given: 終了日と同じ日付が与えられる
   const startDate = '2025-01-01';
   const endDate = '2025-12-31';
   const targetDate = new Date('2025-12-31');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: trueが返される
   expect(result).toBe(true);
 });


 it('範囲外(開始日より前)の場合はfalseを返す', () => {
   // Given: 開始日より前の日付が与えられる
   const startDate = '2025-01-01';
   const endDate = '2025-12-31';
   const targetDate = new Date('2024-12-31');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: falseが返される
   expect(result).toBe(false);
 });


 it('範囲外(終了日より後)の場合はfalseを返す', () => {
   // Given: 終了日より後の日付が与えられる
   const startDate = '2025-01-01';
   const endDate = '2025-12-31';
   const targetDate = new Date('2026-01-01');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: falseが返される
   expect(result).toBe(false);
 });


 it('デフォルトで現在日時を使用する', () => {
   // Given: 現在日時を固定する
   const mockDate = new Date('2025-06-15');
   jest.useFakeTimers();
   jest.setSystemTime(mockDate);


   // When: targetDateを指定せずにisDateInRangeを呼び出す
   const result = isDateInRange('2025-01-01', '2025-12-31');


   // Then: 現在日時に基づいた結果が返される
   expect(result).toBe(true);


   jest.useRealTimers();
 });


 it('開始日と終了日が同じで範囲内の場合はtrueを返す', () => {
   // Given: 開始日と終了日が同じ日付が与えられる
   const startDate = '2025-01-01';
   const endDate = '2025-01-01';
   const targetDate = new Date('2025-01-01');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: trueが返される
   expect(result).toBe(true);
 });


 it('無効な日付文字列(startDate)が渡された場合はfalseを返す', () => {
   // Given: 無効な日付文字列がstartDateとして与えられる
   const startDate = 'invalid-date';
   const endDate = '2025-12-31';
   const targetDate = new Date('2025-06-15');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: falseが返される(Invalid Dateの比較結果)
   expect(result).toBe(false);
 });


 it('無効な日付文字列(endDate)が渡された場合はfalseを返す', () => {
   // Given: 無効な日付文字列がendDateとして与えられる
   const startDate = '2025-01-01';
   const endDate = 'invalid-date';
   const targetDate = new Date('2025-06-15');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: falseが返される(Invalid Dateの比較結果)
   expect(result).toBe(false);
 });


 it('空文字列(startDate)が渡された場合はfalseを返す', () => {
   // Given: 空文字列がstartDateとして与えられる
   const startDate = '';
   const endDate = '2025-12-31';
   const targetDate = new Date('2025-06-15');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: falseが返される(Invalid Dateの比較結果)
   expect(result).toBe(false);
 });


 it('空文字列(endDate)が渡された場合はfalseを返す', () => {
   // Given: 空文字列がendDateとして与えられる
   const startDate = '2025-01-01';
   const endDate = '';
   const targetDate = new Date('2025-06-15');
   // When: isDateInRangeを呼び出す
   const result = isDateInRange(startDate, endDate, targetDate);
   // Then: falseが返される(Invalid Dateの比較結果)
   expect(result).toBe(false);
 });
});


// isWithInIntervalでいいじゃないかという声は置いておいて...

まだ関数によって過不足がありますが、叩き台としては十分ではないでしょうか。

Commandsを打って10秒ほどで完成するわけですから、0から作るのと比べたら圧倒的に楽です。

また、対象の関数に対するフィードバックも出してくれます(エラーハンドリングが不十分など)。

新しく関数を作成した人がCommandsを使用するだけでテストが完成し、レビューの負担も軽くなりますね。

まとめ

最後までご覧いただきありがとうございました。

今回はテストに対する悩みを解消するために作成した、単体テスト自動生成Commandsについて紹介してきましたが、いかがでしたか?

個人的にはテスト作成にかかる工数削減にも成功し、大満足な結果となりました。

皆さんもぜひオリジナルのCommandsを作ってみてください。
きっと将来のあなたや同じ悩みを持つ誰かの助けになります!

サムネイル
【後編】Cursor × Serena MCPで始める仕様駆動開発 ― 要件定義~実装編