メインコンテンツまでスキップ

Cursor を使用したコードテスト:単体テスト、統合テスト、E2E

Cursor Testing

テストは信頼性のあるソフトウェア開発の基盤です。Cursor の AI 機能は、包括的なテストスイートの作成、エッジケースの特定、テストカバレッジの維持に役立ちます。このガイドでは、単体テスト、統合テスト、エンドツーエンドテストのワークフローについて説明します。

前提条件​

開始する前に:

  • Cursor をインストール(最新バージョンを推奨)
  • テストフレームワークが設定されたコードベース
  • テストの概念(アサーション、モック、フィクスチャ)の基礎知識

Cursor でのテスト設定​

プロジェクト構造​

project/
src/
components/
utils/
api/
tests/
unit/ # 単体テスト
integration/ # 統合テスト
e2e/ # エンドツーエンドテスト
jest.config.js
playwright.config.ts
vitest.config.ts

TypeScript 対応 Jest の設定​

// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': 'identity-obj-proxy',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};

AI を使用した単体テストの作成​

コードからテストを生成​

コードスニペットを選択し、Cursor の Ctrl+L コマンドを使用:

この関数の包括的な単体テストを作成してください:
- ハッピーパス
- エッジケース(空の入力、大きな値、特殊文字)
- エラー処理
- 境界条件

例:ユーティリティ関数のテスト​

// src/utils/validation.ts
export const isValidEmail = (email: string): boolean => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};

export const formatDate = (
date: Date,
format: string = 'YYYY-MM-DD'
): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day);
};

Cursor がテストを生成:

// tests/unit/validation.test.ts
import { isValidEmail, formatDate } from '@/utils/validation';

describe('isValidEmail', () => {
it('returns true for valid email', () => {
expect(isValidEmail('[email protected]')).toBe(true);
expect(isValidEmail('[email protected]')).toBe(true);
});

it('returns false for invalid email', () => {
expect(isValidEmail('')).toBe(false);
expect(isValidEmail('not-an-email')).toBe(false);
expect(isValidEmail('@domain.com')).toBe(false);
expect(isValidEmail('user@')).toBe(false);
expect(isValidEmail('user@domain')).toBe(false);
});

it('handles edge cases', () => {
expect(isValidEmail('[email protected]')).toBe(true);
expect(isValidEmail('[email protected]')).toBe(true);
});
});

describe('formatDate', () => {
it('formats date with default format', () => {
const date = new Date('2024-03-15');
expect(formatDate(date)).toBe('2024-03-15');
});

it('formats date with custom format', () => {
const date = new Date('2024-03-15');
expect(formatDate(date, 'DD/MM/YYYY')).toBe('15/03/2024');
expect(formatDate(date, 'MM-DD-YYYY')).toBe('03-15-2024');
});

it('handles edge cases', () => {
const janFirst = new Date('2024-01-01');
expect(formatDate(janFirst)).toBe('2024-01-01');

const dec31 = new Date('2024-12-31');
expect(formatDate(dec31)).toBe('2024-12-31');
});
});

React コンポーネントのテスト​

// tests/unit/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '@/components/Button';

describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});

it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);

fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('is disabled when loading', () => {
render(<Button isLoading>Loading</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});

it('shows loading spinner when isLoading is true', () => {
render(<Button isLoading>Loading</Button>);
expect(screen.getByTestId('spinner')).toBeInTheDocument();
});
});

統合テスト​

API エンドポイントのテスト​

// tests/integration/api.test.ts
import request from 'supertest';
import { app } from '@/app';
import { prisma } from '@/db';

describe('API Integration Tests', () => {
beforeEach(async () => {
await prisma.user.deleteMany();
});

afterAll(async () => {
await prisma.$disconnect();
});

describe('POST /api/users', () => {
it('creates a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'Test User',
password: 'password123',
})
.expect(201);

expect(response.body.data.email).toBe('[email protected]');
});

it('returns 400 for invalid data', async () => {
await request(app)
.post('/api/users')
.send({
email: 'invalid',
name: '',
})
.expect(400);
});
});
});

データベース連携のテスト​

// tests/integration/database.test.ts
import { UserRepository } from '@/repositories/UserRepository';
import { prisma } from '@/db';

describe('UserRepository', () => {
let repository: UserRepository;

beforeEach(() => {
repository = new UserRepository();
});

afterEach(async () => {
await prisma.user.deleteMany();
});

it('creates and retrieves a user', async () => {
const user = await repository.create({
name: 'Test User',
});

expect(user.id).toBeDefined();

const found = await repository.findById(user.id);
expect(found?.email).toBe('[email protected]');
});
});

Playwright を使用したエンドツーエンドテスト​

Playwright のセットアップ​

npm init playwright@latest

E2E テストの生成​

// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication Flow', () => {
test('user can log in', async ({ page }) => {
await page.goto('/login');

await page.fill('[data-testid="email-input"]', '[email protected]');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');

await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toContainText(
'Welcome'
);
});

test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');

await page.fill('[data-testid="email-input"]', '[email protected]');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');

await expect(page.locator('[data-testid="error-message"]')).toContainText(
'Invalid credentials'
);
});
});

Cursor を使用したページオブジェクトパターン​

// tests/e2e/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;

constructor(private page: Page) {
this.emailInput = page.locator('[data-testid="email-input"]');
this.passwordInput = page.locator('[data-testid="password-input"]');
this.loginButton = page.locator('[data-testid="login-button"]');
this.errorMessage = page.locator('[data-testid="error-message"]');
}

async goto() {
await this.page.goto('/login');
}

async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}

Cursor を使用したテスト駆動開発​

赤・緑・リファクタリングサイクル​

  1. 赤:期待する動作を記述するテストを作成
  2. 緑:Cursor を使用して実装コードを生成
  3. リファクタリング:テストが通過したまま Cursor にコードをクリーンアップするよう依頼

例:Cursor を使用した TDD​

// ステップ 1:テストを作成
// tests/unit/calculator.test.ts
import { Calculator } from '@/utils/Calculator';

describe('Calculator', () => {
it('adds two numbers', () => {
const calc = new Calculator();
expect(calc.add(2, 3)).toBe(5);
});

it('handles negative numbers', () => {
const calc = new Calculator();
expect(calc.add(-2, 3)).toBe(1);
});
});
// ステップ 2:Cursor が実装を生成
// src/utils/Calculator.ts
export class Calculator {
add(a: number, b: number): number {
return a + b;
}
}

モックとスタブ​

Jest のモック​

// tests/unit/api.test.ts
import { fetchUserData } from '@/api/users';
import { getDatabaseConnection } from '@/db';

jest.mock('@/db', () => ({
getDatabaseConnection: jest.fn(),
}));

describe('fetchUserData', () => {
it('returns user data from database', async () => {
const mockQuery = jest.fn().mockResolvedValue({
rows: [{ id: 1, name: 'Test User' }],
});

(getDatabaseConnection as jest.Mock).mockReturnValue({
query: mockQuery,
});

const result = await fetchUserData(1);
expect(result).toEqual({ id: 1, name: 'Test User' });
expect(mockQuery).toHaveBeenCalledWith(
'SELECT * FROM users WHERE id = $1',
[1]
);
});
});

API モック用 MSW​

// tests/mocks/handlers.ts
import { rest } from 'msw';

export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json({
users: [
{ id: 1, name: 'Test User' },
{ id: 2, name: 'Another User' },
],
})
);
}),

rest.post('/api/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.status(201),
ctx.json({ id: 3, ...body })
);
}),
];

テストカバレッジと品質​

カバレッジレポートの生成​

# Jest
npx jest --coverage

# Vitest
npx vitest --coverage

# Cursor で表示
# ターミナル内の HTML レポートリンクをクリック

Cursor のカバレッジ分析​

このテストスイートを分析してください:
1. どのエッジケースがテストでカバーされていますか?
2. どのようなテストの空白がありますか?
3. ブランチカバレッジをどのように向上できますか?

継続的インテグレーションのセットアップ​

GitHub Actions​

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run test:unit
- run: npm run test:integration
- run: npm run test:e2e

テストクイックリファレンス​

フレームワーク用途Cursor プロンプト
Jest単体/統合"このコンポーネントの Jest テストを作成する"
Vitest高速単体テスト"エッジケースをカバーする Vitest テストを生成する"
PlaywrightE2E ブラウザ"ログインフローの Playwright テストを作成する"
CypressE2E ブラウザ"フォーム送信の Cypress テストを作成する"
MSWAPI モック"ユーザー API 用の MSW ハンドラを設定する"
React Testing Libraryコンポーネントテスト"RTL を使用してこの React コンポーネントをテストする"

一般的なテストパターン​

パターン例
ファクトリ関数createUser({ role: 'admin' })
セットアップ/クリーンアップbeforeEach / afterEach
パラメータ化テストit.each([[1, 2, 3], [4, 5, 9]])
スナップショットテストexpect(tree).toMatchSnapshot()
スパイjest.spyOn(console, 'error')

関連リソース​

最終更新:2025 年 7 月