【テクニカル・上級編】CursorでのPython・TypeScriptテスト自動生成:単体テストをAIに一括記述させるワークフロー – 軽量・高機能テキストエディタ生産性向上バイブル

伝説のDevOpsアーキテクトが説く:Cursorによるテスト自動生成と、開発ライフサイクル全自動化の要諦

幾多のプロジェクトでCI/CDパイプラインを構築し、数千のテストケースが織りなす緑色のダッシュボードを見つめてきた私だが、近年のAIエディタの進化、特に「Cursor」の登場によって、ソフトウェアエンジニアリングの生産性定義は完全に書き換わった。

「AIにテストを書かせる」という行為は、もはや物珍しいお遊びではない。既存のコードベースから文脈を完璧に読み解き、人間が書き落としがちなエッジケースや複雑なモック化を伴う単体テストを、JestやPytestの流儀に則って一瞬で爆誕させる。このワークフローを開発者のローカルからCI/CDパイプラインまでシームレスに組み込むことこそ、現代のトップエンジニアリングチームに課された至上命題である。

本稿では、Cursorを単なる「賢いコード補完ツール」としてではなく、開発プロセス全体を駆動する自律型エンジンとして骨の髄まで使い倒すための実践的知見を授けよう。

—

1. 内部アーキテクチャの理解:Cursorは何を見ているのか

Cursorの実態は、Fork元であるVS Codeの拡張エコシステムを完全維持しながら、独自の中核AIレイヤー(Composer機能やインデックス生成機構)を深く統合したハイブリッド・エディタである。

ローカルベクトルインデックスとAST解析

Cursorを開いた際、裏側で何が起きているか意識したことはあるだろうか?
Cursorはプロジェクトルートを走査し、抽象構文木(AST)と埋め込みベクトル(Vector Embeddings)をローカルデータベースに構築する。TypeScriptの複雑な型定義や、Pythonの動的なメタプログラミングの構造さえも、このインデックスによってコンテキストとして即座にメモリ上にロードされる。

このアーキテクチャを理解していれば、「なぜAIが的外れなテストコードを出力するのか」の理由が見えてくる。インデックスが古い、あるいは `.cursorignore` の設定が不適切で必要な型定義や共通モジュールが除外されていると、AIは文脈をハルシネーション(幻覚)で埋め合わせようとする。

—

2. 徹底解説:Python (Pytest) & TypeScript (Jest) のテスト自動生成ワークフロー

ここからが本題だ。単に「テストを書いて」とプロンプトを投げる素人仕事を今すぐやめよう。
確実な型安全性とモック化を伴うテストを、Cursorの `.cursorrules` とカスタムプロンプトを用いて「一撃」で生成させる環境を構築する。

2.1 開発環境の規程:`.cursorrules` の極意

プロジェクトのルートディレクトリに `.cursorrules` を配置する。これにより、AIが吐き出すテストコードの品質が劇的に安定する。

.cursorrules – Cursor AI Behavior Configuration

Python (Pytest) Guidelines

  • Test files must be named `test_.py` and placed in a `tests/` directory mirroring the source structure.
  • Use `pytest` and `pytest-mock` for mocking dependencies.
  • Every async function must be tested using `pytest-asyncio`.
  • Explicitly cover edge cases: None values, empty collections, division by zero, and network timeouts.
  • Type hints are mandatory for all test helper functions.

TypeScript (Jest) Guidelines

  • Test files must be named `.spec.ts` or `.test.ts` adjacent to the source or in a `__tests__` directory.
  • Use `jest` with `ts-jest`. Mock external services using `jest.mock()` with explicit factory functions.
  • Test both success and failure (rejection) paths for all Promises.
  • Avoid `any` types in test code; leverage strict typing for mock objects (e.g., `jest.Mocked`).

この設定ファイルを置くだけで、Cursorは自発的にプロジェクトのコーディング規約を強制し、散らかったテストコードの生成を防ぐ。

—

2.2 実践:TypeScript (Jest) での複雑な非同期・モックテスト自動生成

以下のような、外部APIを叩くTypeScriptのサービスクラスがあるとする。

// src/services/payment.service.ts
import { HttpClient } from ‘../utils/http-client’;
import { Logger } from ‘../utils/logger’;

export interface PaymentPayload {
amount: number;
currency: string;
sourceToken: string;
}

export class PaymentService {
constructor(private http: HttpClient, private logger: Logger) {}

async processPayment(payload: PaymentPayload): Promise {
if (payload.amount <= 0) { throw new Error('Invalid payment amount'); } try { this.logger.info(`Processing payment: ${payload.amount} ${payload.currency}`); const response = await this.http.post<{ transactionId: string }>(‘/charge’, payload);
return response.transactionId;
} catch (error) {
this.logger.error(`Payment failed: ${(error as Error).message}`);
throw new Error(‘Payment gateway error’);
}
}
}

Cursor Composer (Ctrl+I / Cmd+I) を使った一括生成手順

1. `src/services/payment.service.ts` をアクティブタブにする。
2. `Cmd + I` (Composer) を起動し、以下のプロンプトを入力する。

> Prompt:
> 「このクラスに対する網羅的なJestの単体テストコードを生成せよ。HttpClientとLoggerは完全にモック化し、以下のケースを必ず含めること:
> 1. 正常系:ペイメントが成功し、transactionIdが返却されること。
> 2. 異常系:amountが0以下の場合に即座にErrorがスローされること(APIが呼ばれないことの検証)。
> 3. 異常系:HttpClientが例外をスローした際、Loggerにエラーが記録され、カスタムエラーが再スローされること。」

生成されるテストコードの例

Cursorは `.cursorrules` を解釈し、以下のような極めてクリーンなJestコードを一瞬で生成する。

// src/services/__tests__/payment.service.spec.ts
import { PaymentService, PaymentPayload } from ‘../payment.service’;
import { HttpClient } from ‘../../utils/http-client’;
import { Logger } from ‘../../utils/logger’;

// HttpClientとLoggerのモック化
jest.mock(‘../../utils/http-client’);
jest.mock(‘../../utils/logger’);

describe(‘PaymentService’, () => {
let paymentService: PaymentService;
let mockHttpClient: jest.Mocked;
let mockLogger: jest.Mocked;

beforeEach(() => {
// 各テストケースの前にモックを初期化
jest.clearAllMocks();
mockHttpClient = new HttpClient() as jest.Mocked;
mockLogger = new Logger() as jest.Mocked;
paymentService = new PaymentService(mockHttpClient, mockLogger);
});

describe(‘processPayment’, () => {
const validPayload: PaymentPayload = {
amount: 1000,
currency: ‘JPY’,
sourceToken: ‘tok_123’,
};

it(‘should process payment successfully and return transaction ID’, async () => {
// モックの振る舞い定義:APIが正常レスポンスを返す
mockHttpClient.post.mockResolvedValueOnce({ transactionId: ‘txn_999’ });

const result = await paymentService.processPayment(validPayload);

expect(result).toBe(‘txn_999’);
expect(mockLogger.info).toHaveBeenCalledTimes(1);
expect(mockHttpClient.post).toHaveBeenCalledWith(‘/charge’, validPayload);
});

it(‘should throw an error when amount is zero or negative’, async () => {
const invalidPayload = { …validPayload, amount: 0 };

// 異常系の検証(非同期エラー)
await expect(paymentService.processPayment(invalidPayload)).rejects.toThrow(
‘Invalid payment amount’
);
// APIが呼ばれていないことを担保
expect(mockHttpClient.post).not.toHaveBeenCalled();
});

it(‘should log error and throw custom error when HTTP request fails’, async () => {
const apiError = new Error(‘Network timeout’);
mockHttpClient.post.mockRejectedValueOnce(apiError);

await expect(paymentService.processPayment(validPayload)).rejects.toThrow(
‘Payment gateway error’
);
expect(mockLogger.error).toHaveBeenCalledTimes(1);
});
});
});

このコードの美しさは、型安全性が完全に担保され、Jestのベストプラクティス(`jest.clearAllMocks` の配置、`rejects.toThrow` の使用など)が網羅されている点にある。

—

2.3 実践:Python (Pytest) でのエッジケース網羅型テスト自動生成

次に、Python環境でのアプローチだ。データベースセッションや例外処理を伴うサービスクラスを想定する。

app/services/user_service.py
from typing import Optional
from sqlalchemy.orm import Session
from app.models.user import User
from app.schemas.user import UserCreate

class UserService:
def __init__(self, db: Session):
self.db = db

def get_user_by_email(self, email: str) -> Optional[User]:
if not email or “@” not in email:
raise ValueError(“Invalid email format”)
return self.db.query(User).filter(User.email == email).first()

def create_user(self, payload: UserCreate) -> User:
existing = self.get_user_by_email(payload.email)
if existing:
raise ValueError(“User already exists”)

db_user = User(email=payload.email, name=payload.name)
self.db.add(db_user)
self.db.commit()
self.db.refresh(db_user)
return db_user

Cursor CLI / Agent を活用した自動化

Cursorの背景にある強力なコンテキスト理解力を使い、ターミナルから、あるいはエディタ内のチャットで次のように指示する。

> Prompt:
> `app/services/user_service.py` に対する `pytest` 用の単体テストを `tests/services/test_user_service.py` として作成せよ。`pytest-mock` を用い、SQLAlchemyのSession(query, add, commit, refresh)を完全にモック化すること。メールアドレスのバリデーションエラー、重複ユーザーのエラー、正常なユーザー作成フローを網羅すること。

生成されるPytestコードの例

tests/services/test_user_service.py
import pytest
from unittest.mock import MagicMock
from sqlalchemy.orm import Session
from app.services.user_service import UserService
from app.models.user import User
from app.schemas.user import UserCreate

@pytest.fixture
def mock_db():
“””SQLAlchemyセッションのモックフィクスチャ”””
return MagicMock(spec=Session)

@pytest.fixture
def user_service(mock_db):
“””UserServiceのインスタンスを生成するフィクスチャ”””
return UserService(db=mock_db)

def test_get_user_by_email_invalid_format(user_service):
“””不正なメールフォーマットの場合にValueErrorが送出されることの検証”””
with pytest.raises(ValueError, match=”Invalid email format”):
user_service.get_user_by_email(“invalid-email”)

def test_get_user_by_email_success(user_service, mock_db):
“””正常にメールアドレスからユーザーが取得できることの検証”””
mock_query = mock_db.query.return_value
mock_query.filter.return_value.first.return_value = User(id=1, email=”test@example.com”, name=”Test”)

user = user_service.get_user_by_email(“test@example.com”)

assert user is not None
assert user.email == “test@example.com”
mock_db.query.assert_called_once_with(User)

def test_create_user_already_exists(user_service, mock_db):
“””既に存在するメールアドレスでのユーザー作成時にエラーとなることの検証”””
# 既存ユーザーが存在するモック振る舞い
mock_query = mock_db.query.return_value
mock_query.filter.return_value.first.return_value = User(id=1, email=”existing@example.com”, name=”Existing”)

payload = UserCreate(email=”existing@example.com”, name=”New User”)

with pytest.raises(ValueError, match=”User already exists”):
user_service.create_user(payload)

# commitが呼ばれていないことを確認
mock_db.commit.assert_not_called()

—

3. CI/CDパイプラインとの高度な連携:自動テスト生成の完全自動化

ローカルでのテスト生成を自動化したら、次に行うべきは「新機能がブランチにプッシュされた際、AIが自動で未テスト部分のテストコードを生成し、Pull Requestにコミットする」という最高峰のDevOpsパイプラインの構築だ。

Cursor自体はGUIエディタであるが、その裏で動いているAIモデルのAPI(Anthropic Claude 3.5 SonnetやOpenAI GPT-4oなど)を叩くカスタムCLIスクリプトをCI/CDに組み込むことで、この夢を現実にできる。

3.1 AIテスト自動生成CLIスクリプト(Python版)

以下のスクリプトをプロジェクト内に配置し、GitHub Actions等のCIから実行する。これにより、Gitの差分(Diff)を検出し、変更されたソースコードに対してAIがテストファイルを動的に生成・上書きする。

scripts/generate_tests_ai.py
import os
import subprocess
import sys
from anthropic import Anthropic

client = Anthropic(api_key=os.environ.get(“ANTHROPIC_API_KEY”))

def get_git_diff() -> str:
“””直近のコミットやステージングされていない変更の差分を取得する”””
result = subprocess.run([“git”, “diff”, “–cached”, “–name-only”], capture_output=True, text=True, check=True)
files = result.stdout.splitlines()
# ソースファイルのみを抽出(テストファイルや設定ファイルを除外)
source_files = [f for f in files if (f.startswith(“src/”) or f.startswith(“app/”)) and not f.endswith((“.spec.ts”, “.test.ts”, “_test.py”))]
return source_files

def generate_test_for_file(filepath: str):
“””Anthropic APIを使用して指定ファイルのテストコードを自動生成する”””
if not os.path.exists(filepath):
return

with open(filepath, “r”, encoding=”utf-8″) as f:
code_content = f.read()

prompt = f”””
あなたは世界最高峰のQAエンジニアです。以下のソースコードに対する完全な単体テストコードを生成してください。
出力はテストコードのファイル内容のみとし、Markdownのバッククォート等を含めない純粋なコードを出力してください。

Target File: {filepath}
Source Code:
{code_content}
“””

print(f”Generating tests for {filepath} using AI…”)
response = client.messages.create(
model=”claude-3-5-sonnet-20241022″,
max_tokens=4000,
messages=[{“role”: “user”, “content”: prompt}]
)

test_code = response.content[0].text

# テストファイルのパスを決定 (例: src/services/foo.ts -> src/services/__tests__/foo.spec.ts)
dir_name, file_name = os.path.split(filepath)
base_name, _ = os.path.splitext(file_name)

if “src/” in filepath:
test_dir = os.path.join(dir_name, “__tests__”)
os.makedirs(test_dir, exist_ok=True)
test_filepath = os.path.join(test_dir, f”{base_name}.spec.ts”)
else:
test_dir = “tests”
os.makedirs(test_dir, exist_ok=True)
test_filepath = os.path.join(test_dir, f”test_{base_name}.py”)

with open(test_filepath, “w”, encoding=”utf-8″) as f:
f.write(test_code)
print(f”Successfully generated: {test_filepath}”)

if __name__ == “__main__”:
files = get_git_diff()
if not files:
print(“No source files changed for test generation.”)
sys.exit(0)

for f in files:
generate_test_for_file(f)

3.2 GitHub Actionsワークフローの設定

このスクリプトをCI/CDに組み込む `.github/workflows/auto-test-gen.yml` の設計図だ。

name: AI Test Generation Pipeline

on:
pull_request:
types: [opened, synchronize]

jobs:
generate-tests:
runs-on: ubuntu-latest
permissions:
contents: write # PRへのコミット権限を付与
steps:

  • name: Checkout Repository

uses: actions/checkout@v4
with:
persist-credentials: true
fetch-depth: 0

  • name: Set up Python

uses: actions/setup-python@v5
with:
python-version: ‘3.11’

  • name: Install Dependencies

run: |
pip install anthropic

  • name: Run AI Test Generator

env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# 直近のコミットで変更されたファイルをステージングしてスクリプトを実行
git diff –name-only HEAD~1 HEAD > changed_files.txt
python scripts/generate_tests_ai.py

  • name: Commit and Push Generated Tests

run: |
git config –global user.name “github-actions[bot]”
git config –global user.email “github-actions[bot]@users.noreply.github.com”
git add .
git status
# 変更がある場合のみコミット&プッシュ
git diff –staged –quiet || (git commit -m “chore(ai): auto-generate unit tests via Claude 3.5 Sonnet” && git push)

このパイプラインが稼働する環境において、開発者がビジネスロジックを実装してPRを投げれば、数秒後にAIが完璧なテストコードを自動で記述し、同じPRへ自動コミットしてくれる。人間はレビューとエッジケースの微調整を行うだけでよい。

—

4. パフォーマンス最適化ハック:大規模プロジェクトにおけるCursorのメモリ・CPU枯渇を防ぐ

最後に、大規模なコードベース(数百万行規模のモノレポなど)において、Cursorが重くなったり、インデックス生成がフリーズしたりする現象に悩むエンジニアへ向けた、現場の最適化ハックを伝授する。

4.1 `.cursorignore` による不要なインデックスの徹底排除

Cursorはデフォルトで `.gitignore` の内容を尊重するが、巨大なビルド成果物、サードパーティの型定義、バイナリ、ログファイルなどは明示的に `.cursorignore` で除外しなければ、ローカルのベクトル検索インデックスが肥大化し、AIの回答精度低下とメモリ爆食いを引き起こす。

.cursorignore
巨大なビルド成果物やキャッシュディレクトリ
dist/
build/
.next/
coverage/
__pycache__/
.pytest_cache/
.mypy_cache/

大規模なサードパーティ製データや自動生成スキーマ
node_modules/
vendor/
.min.js
.bundle.js

機密情報やログ
.log
.env
secrets/

4.2 インデックスの再構築(Reindex)のタイミング

コードベース全体を大規模にリファクタリングした直後、Cursorの動作が怪しくなったり、古いメソッドを参照してテストコードを生成し始めた場合は、手動でインデックスをリフレッシュする必要がある。

  • コマンドパレット (`Ctrl + Shift + P` / `Cmd + Shift + P`) を開き、`Cursor: Regenerate Index` を実行せよ。これにより、ローカルのベクトルデータベースがクリーンアップされ、最新のAST構造に基づいた高精度なAIコンテキストが復活する。

—

結び:ツールに踊らされるな、ツールを飼い慣らせ

AIエディタは、使いこなせば最強の相棒となるが、野放しにすれば的外れなコードの山を生み出す諸刃の剣だ。
`.cursorrules` による厳格なルール統制、CI/CDパイプラインへのテスト自動生成スクリプトの組み込み、そしてインデックスの適切なチューニング。これらすべてのレイヤーを俯瞰し、制御下に置くことのできるエンジニアだけが、圧倒的なスピードと品質を両立させた「真のモダン開発」を謳歌できる。

さあ、エディタを開け。君の手で、テストを書くという作業そのものを過去の遺物にしよう。

タイトルとURLをコピーしました