はじめに

テストはソフトウェア品質を担保する重要な要素ですが、テストコードの作成には多くの時間と労力がかかります。Cursorを活用すれば、AIがコードを分析してテストケースを自動生成し、エッジケースも提案してくれます。

本記事では、Cursorを使ったテスト作成の効率化手法を解説します。この記事を読むことで、以下のことができるようになります。

  • テストされていないコード(テストギャップ)を特定できる
  • 関数やクラスに対するテストケースを自動生成できる
  • エッジケースや境界値テストの提案を受けられる
  • 既存テストパターンに沿った一貫性のあるテストを作成できる
  • ターミナル連携でテスト実行と修正を効率的に行える

実行環境と前提条件

実行環境

項目 要件
オペレーティングシステム Windows 10以上、macOS 10.15以上、Ubuntu 20.04以上
Cursor バージョン 2.3以降(2026年1月時点の安定版)
インターネット接続 必須(AIモデル利用に必要)
テストフレームワーク Jest、Vitest、Mocha、pytest等(言語に応じて)

前提条件

  • Cursorがインストールされ、アカウントでログイン済みであること
  • テスト対象のプロジェクトが開かれていること
  • テストフレームワークがプロジェクトにセットアップされていること
  • 基本的なテストの概念(単体テスト、結合テスト等)を理解していること

期待される結果

本記事の手順を完了すると、以下の状態になります。

  • テストカバレッジのギャップを効率的に特定できる
  • AIによるテストケース生成で作業時間を大幅に短縮できる
  • エッジケースを網羅した堅牢なテストスイートを構築できる
  • テスト実行から修正までのサイクルを高速に回せる

テストされていないコードの特定

テストカバレッジを向上させる第一歩は、テストされていないコードを特定することです。

Agentによるテストギャップ分析

Agent(Ctrl+I)を使って、プロジェクト全体のテストカバレッジ状況を分析できます。

1
2
3
4
指示例:
@Codebase このプロジェクトのテストカバレッジ状況を分析してください。
テストファイルが存在しない、または不十分なモジュールを特定し、
優先度の高い順にリストアップしてください。

Agentは以下のような分析結果を提供します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Agentの分析結果例:

## テストカバレッジ分析

### テスト未作成のモジュール(優先度: 高)
1. src/services/payment.service.ts
   - 決済処理の中核ロジック
   - エラーハンドリングが複雑
   
2. src/utils/validation.ts
   - 入力バリデーション関数群
   - 30以上の関数が未テスト

### テスト不足のモジュール(優先度: 中)
1. src/services/user.service.ts
   - 既存テスト: 5件
   - カバー率推定: 40%
   - 未テスト: パスワードリセット、権限チェック

2. src/controllers/order.controller.ts
   - 既存テスト: 3件
   - カバー率推定: 25%
   - 未テスト: エラーレスポンス、ページネーション

カバレッジレポートとの連携

既存のカバレッジレポートをAgentに共有して、より詳細な分析を依頼することもできます。

1
2
3
4
5
指示例:
以下のカバレッジレポートを分析して、
テストを追加すべき優先度の高い関数を特定してください。

@coverage/lcov-report/index.html

テストファイルの存在確認

特定のソースファイルに対応するテストファイルの有無を確認します。

1
2
3
指示例:
@src/services/order.service.ts に対応するテストファイルは存在しますか?
存在しない場合は、テストファイルの雛形を作成してください。

テストケースの自動生成

Cursorは、関数やクラスのコードを分析して、適切なテストケースを自動生成します。

基本的なテスト生成

対象ファイルを@メンションして、テスト生成を依頼します。

1
2
3
4
指示例:
@src/utils/string-utils.ts このファイルの関数に対する
単体テストを作成してください。Jest形式で、
各関数に対して正常系と異常系のテストを含めてください。

対象コード例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/utils/string-utils.ts
export function capitalize(str: string): string {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

export function truncate(str: string, maxLength: number): string {
  if (str.length <= maxLength) return str;
  return str.slice(0, maxLength - 3) + '...';
}

export function slugify(str: string): string {
  return str
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_-]+/g, '-')
    .replace(/^-+|-+$/g, '');
}

生成されるテスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// src/utils/string-utils.test.ts
import { capitalize, truncate, slugify } from './string-utils';

describe('string-utils', () => {
  describe('capitalize', () => {
    it('文字列の先頭を大文字にする', () => {
      expect(capitalize('hello')).toBe('Hello');
    });

    it('残りの文字を小文字にする', () => {
      expect(capitalize('hELLO')).toBe('Hello');
    });

    it('空文字列の場合は空文字列を返す', () => {
      expect(capitalize('')).toBe('');
    });

    it('nullやundefinedの場合は空文字列を返す', () => {
      expect(capitalize(null as unknown as string)).toBe('');
      expect(capitalize(undefined as unknown as string)).toBe('');
    });

    it('1文字の場合は大文字にする', () => {
      expect(capitalize('a')).toBe('A');
    });
  });

  describe('truncate', () => {
    it('指定した長さ以下の文字列はそのまま返す', () => {
      expect(truncate('hello', 10)).toBe('hello');
    });

    it('指定した長さを超える文字列は省略する', () => {
      expect(truncate('hello world', 8)).toBe('hello...');
    });

    it('境界値: ちょうど最大長の場合はそのまま返す', () => {
      expect(truncate('hello', 5)).toBe('hello');
    });

    it('最大長が3以下の場合も正しく動作する', () => {
      expect(truncate('hello', 3)).toBe('...');
    });
  });

  describe('slugify', () => {
    it('スペースをハイフンに変換する', () => {
      expect(slugify('Hello World')).toBe('hello-world');
    });

    it('特殊文字を除去する', () => {
      expect(slugify('Hello, World!')).toBe('hello-world');
    });

    it('連続するハイフンを1つにまとめる', () => {
      expect(slugify('Hello   World')).toBe('hello-world');
    });

    it('先頭と末尾のハイフンを除去する', () => {
      expect(slugify('-Hello World-')).toBe('hello-world');
    });

    it('日本語文字を除去する', () => {
      expect(slugify('Hello こんにちは World')).toBe('hello-world');
    });
  });
});

クラスのテスト生成

クラスに対しては、メソッドごとにテストを生成し、モック設定も含めます。

1
2
3
4
5
指示例:
@src/services/order.service.ts このサービスクラスのテストを作成してください。
- 依存関係はモック化
- 各メソッドの正常系・異常系をカバー
- TypeScriptの型を活用

生成されるサービステスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// src/services/order.service.test.ts
import { OrderService } from './order.service';
import { OrderRepository } from '../repositories/order.repository';
import { ProductService } from './product.service';
import { Order, OrderStatus } from '../entities/order.entity';

// モックの作成
jest.mock('../repositories/order.repository');
jest.mock('./product.service');

describe('OrderService', () => {
  let orderService: OrderService;
  let orderRepository: jest.Mocked<OrderRepository>;
  let productService: jest.Mocked<ProductService>;

  beforeEach(() => {
    orderRepository = new OrderRepository() as jest.Mocked<OrderRepository>;
    productService = new ProductService() as jest.Mocked<ProductService>;
    orderService = new OrderService(orderRepository, productService);
    jest.clearAllMocks();
  });

  describe('createOrder', () => {
    const createOrderDto = {
      userId: 'user-1',
      items: [
        { productId: 'product-1', quantity: 2 },
        { productId: 'product-2', quantity: 1 },
      ],
    };

    it('正常に注文を作成できる', async () => {
      productService.checkStock.mockResolvedValue(true);
      productService.getPrice.mockResolvedValue(1000);
      orderRepository.save.mockResolvedValue({
        id: 'order-1',
        ...createOrderDto,
        status: OrderStatus.PENDING,
        totalAmount: 3000,
      } as Order);

      const result = await orderService.createOrder(createOrderDto);

      expect(result.id).toBe('order-1');
      expect(result.status).toBe(OrderStatus.PENDING);
      expect(productService.checkStock).toHaveBeenCalledTimes(2);
    });

    it('在庫不足の場合はエラーを投げる', async () => {
      productService.checkStock.mockResolvedValue(false);

      await expect(orderService.createOrder(createOrderDto))
        .rejects
        .toThrow('在庫が不足しています');
    });

    it('空の注文アイテムの場合はエラーを投げる', async () => {
      await expect(orderService.createOrder({ userId: 'user-1', items: [] }))
        .rejects
        .toThrow('注文アイテムが必要です');
    });
  });

  describe('updateStatus', () => {
    it('ステータスを更新できる', async () => {
      const existingOrder = {
        id: 'order-1',
        status: OrderStatus.PENDING,
      } as Order;
      
      orderRepository.findById.mockResolvedValue(existingOrder);
      orderRepository.save.mockResolvedValue({
        ...existingOrder,
        status: OrderStatus.CONFIRMED,
      } as Order);

      const result = await orderService.updateStatus('order-1', OrderStatus.CONFIRMED);

      expect(result.status).toBe(OrderStatus.CONFIRMED);
    });

    it('存在しない注文の場合はエラーを投げる', async () => {
      orderRepository.findById.mockResolvedValue(null);

      await expect(orderService.updateStatus('invalid-id', OrderStatus.CONFIRMED))
        .rejects
        .toThrow('注文が見つかりません');
    });
  });
});

エッジケースの提案

Cursorは、コードを分析して見落としがちなエッジケースを提案します。

エッジケース分析の依頼

1
2
3
4
指示例:
@src/utils/date-utils.ts この関数群に対して、
見落としがちなエッジケースを提案してください。
境界値、特殊な入力値、タイムゾーンの考慮などを含めてください。

提案されるエッジケースの例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Agentの提案例:

## calculateAge関数のエッジケース

### 境界値テスト
- 誕生日当日(年齢が変わる瞬間)
- 誕生日の前日
- うるう年の2月29日生まれ

### 特殊な入力値
- 未来の日付(エラーハンドリング確認)
- 1900年以前の日付
- 無効な日付文字列

### タイムゾーン考慮
- UTCと日本時間で日付が異なる場合
- サマータイム境界

### 以下のテストケースを追加することを推奨します:

提案されたエッジケースのテスト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
describe('calculateAge - エッジケース', () => {
  beforeEach(() => {
    // 現在日時を固定
    jest.useFakeTimers();
    jest.setSystemTime(new Date('2026-01-09'));
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  describe('境界値', () => {
    it('誕生日当日は年齢が加算される', () => {
      expect(calculateAge(new Date('1990-01-09'))).toBe(36);
    });

    it('誕生日前日はまだ年齢が加算されない', () => {
      expect(calculateAge(new Date('1990-01-10'))).toBe(35);
    });

    it('うるう年2月29日生まれの場合、3月1日に年齢が加算される', () => {
      jest.setSystemTime(new Date('2026-02-28'));
      expect(calculateAge(new Date('2000-02-29'))).toBe(25);
      
      jest.setSystemTime(new Date('2026-03-01'));
      expect(calculateAge(new Date('2000-02-29'))).toBe(26);
    });
  });

  describe('特殊な入力値', () => {
    it('未来の日付はエラーを投げる', () => {
      expect(() => calculateAge(new Date('2030-01-01')))
        .toThrow('生年月日は過去の日付である必要があります');
    });

    it('100歳以上の日付も正しく計算する', () => {
      expect(calculateAge(new Date('1920-01-01'))).toBe(106);
    });

    it('無効な日付はエラーを投げる', () => {
      expect(() => calculateAge(new Date('invalid')))
        .toThrow('無効な日付です');
    });
  });
});

網羅的なテストケース生成

特定の関数に対して、すべてのパターンを網羅するテストを依頼できます。

1
2
3
4
指示例:
@src/utils/validation.ts の validateEmail 関数に対して、
RFC 5322準拠のテストケースを網羅的に作成してください。
有効なメールアドレスと無効なメールアドレスの両方を含めてください。

既存テストパターンに沿ったテスト作成

プロジェクトに既存のテストがある場合、そのパターンに沿った一貫性のあるテストを作成できます。

パターン分析と適用

1
2
3
4
指示例:
@src/services/__tests__ このディレクトリの既存テストパターンを分析し、
@src/services/notification.service.ts のテストを
同じパターンで作成してください。

既存パターンの認識要素

Cursorは以下のパターンを認識して適用します。

パターン要素
ファイル命名規則 *.test.ts, *.spec.ts
テスト構造 describe/it のネスト構造
セットアップ方法 beforeEach, beforeAll の使い方
モック手法 jest.mock, jest.spyOn の使い方
アサーションスタイル expect().toBe(), assert.equal()
データ作成方法 Factory, Fixture, Builder パターン

テストファクトリーの活用

既存のファクトリーパターンを認識して活用します。

1
2
3
4
5
6
7
8
9
// 既存のファクトリー
// src/__tests__/factories/user.factory.ts
export const createTestUser = (overrides?: Partial<User>): User => ({
  id: 'user-test-id',
  email: 'test@example.com',
  name: 'Test User',
  createdAt: new Date('2026-01-01'),
  ...overrides,
});
1
2
3
指示例:
既存のUserファクトリーパターンに従って、
Orderのテストファクトリーを作成してください。

ターミナル連携によるテスト実行と修正フロー

Cursorはターミナルと連携して、テストの実行から修正までのサイクルを効率化します。

テスト実行の依頼

1
2
3
指示例:
作成したテストをターミナルで実行して、
結果を確認してください。

Agentはターミナルでテストコマンドを実行します。

1
npm test -- src/services/order.service.test.ts

失敗テストの自動修正

テストが失敗した場合、Agentにエラーの分析と修正を依頼できます。

1
2
3
4
5
6
7
8
9
指示例:
テストが失敗しました。エラー内容を分析して、
テストコードまたは実装コードを修正してください。

エラー:
FAIL src/services/order.service.test.ts
  ● OrderService › createOrder › 正常に注文を作成できる
    Expected: 3000
    Received: undefined

テスト実行と修正のワークフロー

flowchart TD
    A[テスト作成] --> B[テスト実行]
    B --> C{結果確認}
    C -->|成功| D[次のテストへ]
    C -->|失敗| E[エラー分析]
    E --> F{原因特定}
    F -->|テストが誤り| G[テスト修正]
    F -->|実装が誤り| H[実装修正]
    G --> B
    H --> B
    D --> I{全テスト完了?}
    I -->|いいえ| A
    I -->|はい| J[カバレッジ確認]
    J --> K{目標達成?}
    K -->|いいえ| L[追加テスト特定]
    L --> A
    K -->|はい| M[完了]

Watch モードでの継続的テスト

1
2
3
指示例:
テストをWatchモードで実行してください。
ファイルを変更するたびに関連テストが再実行されるようにしてください。
1
npm test -- --watch src/services/

実践的なテスト作成ワークフロー

効率的にテストカバレッジを向上させるワークフローを紹介します。

TDDアプローチとの組み合わせ

Cursorを使ってTDD(テスト駆動開発)を実践できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
TDDワークフローの指示例:

ステップ1:
新機能「クーポン適用」のテストを先に書いてください。
- 有効なクーポンで割引が適用される
- 期限切れクーポンはエラー
- 使用済みクーポンはエラー
- 最低購入金額未満はエラー

ステップ2:
テストが通るように最小限の実装を作成してください。

ステップ3:
実装をリファクタリングしてください。

カバレッジ目標の設定

1
2
3
4
指示例:
@src/services/payment.service.ts のテストカバレッジを
80%以上にしたいです。現在のカバレッジを分析し、
不足している部分のテストを追加してください。

スナップショットテストの活用

コンポーネントや複雑なオブジェクトにはスナップショットテストを使います。

1
2
3
指示例:
@src/components/OrderSummary.tsx のスナップショットテストを作成してください。
様々なpropsパターンに対するスナップショットを含めてください。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
describe('OrderSummary', () => {
  it('通常の注文を正しくレンダリングする', () => {
    const order = createTestOrder();
    const { container } = render(<OrderSummary order={order} />);
    expect(container).toMatchSnapshot();
  });

  it('割引適用時のレンダリング', () => {
    const order = createTestOrder({ discount: 500 });
    const { container } = render(<OrderSummary order={order} />);
    expect(container).toMatchSnapshot();
  });

  it('空の注文のレンダリング', () => {
    const order = createTestOrder({ items: [] });
    const { container } = render(<OrderSummary order={order} />);
    expect(container).toMatchSnapshot();
  });
});

よくあるトラブルと対処法

モックが正しく動作しない

1
2
3
4
5
6
指示例:
以下のモックが正しく動作していません。
原因を分析して修正してください。

jest.mock('../repositories/user.repository');
// モックされた関数が実際の実装を呼んでしまう

非同期テストがタイムアウトする

1
2
3
4
5
6
7
指示例:
このテストがタイムアウトしています。
Promiseの解決待ちや非同期処理に問題がないか確認し、修正してください。

it('大量データを処理できる', async () => {
  // 30秒でタイムアウト
});

テストの実行順序に依存する問題

1
2
3
4
指示例:
テストを個別に実行すると成功しますが、
全体実行すると失敗します。
テスト間の依存関係を特定し、独立したテストに修正してください。

まとめ

Cursorを活用したテスト作成は、テストカバレッジの向上と品質の高いテストコード作成を効率化するAI駆動開発の重要な手法です。本記事で解説した内容をまとめます。

  • テストギャップの特定: Agentによるプロジェクト全体のカバレッジ分析
  • テストケースの自動生成: 関数・クラスに対する正常系・異常系テストの生成
  • エッジケースの提案: 境界値、特殊入力、タイムゾーンなど見落としがちなケース
  • 既存パターンの適用: プロジェクトの規約に沿った一貫性のあるテスト
  • ターミナル連携: テスト実行から修正までのサイクル効率化

これらのテクニックを活用することで、テスト作成にかかる時間を大幅に短縮しながら、より堅牢なテストスイートを構築できます。

参考リンク