テスト API
テスト APIは、開発環境内でテストの実行、管理、レポート作成を行うための包括的な機能を提供します。
概要
テスト APIでは以下のことができます:
- 単体テスト、統合テスト、E2Eテストの実行
- テストスイートとテストケースの管理
- テストカバレッジの測定とレポート作成
- テスト結果の可視化と分析
- 継続的インテグレーション(CI)との統合
- テストの並列実行とパフォーマンス最適化
- テストデータとモックの管理
- テストの自動化とスケジューリング
基本的な使用方法
テストランナー
typescript
import { TraeAPI } from '@trae/api';
// テストランナーの実装
class TestRunner {
private testSuites: Map<string, TestSuite> = new Map();
private testResults: Map<string, TestResult> = new Map();
private testListeners: TestListener[] = [];
private isRunning: boolean = false;
private currentSession: TestSession | null = null;
constructor() {
this.setupTestEnvironment();
this.loadTestConfiguration();
this.initializeTestFrameworks();
}
private setupTestEnvironment(): void {
// テスト環境の設定
console.log('テスト環境を設定中...');
// 環境変数の設定
process.env.NODE_ENV = 'test';
process.env.TEST_MODE = 'true';
// テスト用のグローバル設定
if (typeof global !== 'undefined') {
global.TEST_RUNNER = this;
global.expect = this.createExpectFunction();
global.describe = this.createDescribeFunction();
global.it = this.createItFunction();
global.beforeEach = this.createBeforeEachFunction();
global.afterEach = this.createAfterEachFunction();
}
}
private loadTestConfiguration(): void {
// テスト設定を読み込み
const config = TraeAPI.workspace.getConfiguration('testing');
this.testConfig = {
timeout: config.get('timeout', 5000),
retries: config.get('retries', 0),
parallel: config.get('parallel', false),
coverage: config.get('coverage.enabled', false),
coverageThreshold: config.get('coverage.threshold', 80),
testPattern: config.get('testPattern', '**/*.test.{js,ts}'),
excludePattern: config.get('excludePattern', '**/node_modules/**'),
setupFiles: config.get('setupFiles', []),
teardownFiles: config.get('teardownFiles', [])
};
}
private initializeTestFrameworks(): void {
// サポートされているテストフレームワークを初期化
this.frameworks = new Map([
['jest', new JestFramework()],
['mocha', new MochaFramework()],
['vitest', new VitestFramework()],
['playwright', new PlaywrightFramework()],
['cypress', new CypressFramework()]
]);
}
// テストの実行
async runTests(options: TestRunOptions = {}): Promise<TestSession> {
if (this.isRunning) {
throw new Error('テストは既に実行中です');
}
try {
this.isRunning = true;
// テストセッションを開始
const session = await this.startTestSession(options);
this.currentSession = session;
// テストリスナーに通知
this.notifyTestStart(session);
console.log(`テストセッションを開始: ${session.id}`);
// テストファイルを検索
const testFiles = await this.discoverTestFiles(options);
console.log(`${testFiles.length}個のテストファイルが見つかりました`);
// テストスイートを読み込み
const testSuites = await this.loadTestSuites(testFiles);
// テストを実行
const results = await this.executeTests(testSuites, options);
// 結果を集計
const summary = this.generateTestSummary(results);
session.summary = summary;
session.endTime = new Date();
session.status = summary.failed > 0 ? 'failed' : 'passed';
// テストリスナーに通知
this.notifyTestComplete(session);
console.log(`テストセッション完了: ${session.status}`);
console.log(`合格: ${summary.passed}, 失敗: ${summary.failed}, スキップ: ${summary.skipped}`);
return session;
} catch (error) {
console.error('テスト実行中にエラーが発生しました:', error);
if (this.currentSession) {
this.currentSession.status = 'error';
this.currentSession.error = error.message;
this.currentSession.endTime = new Date();
this.notifyTestError(this.currentSession, error);
}
throw error;
} finally {
this.isRunning = false;
this.currentSession = null;
}
}
private async startTestSession(options: TestRunOptions): Promise<TestSession> {
const session: TestSession = {
id: `test-session-${Date.now()}`,
startTime: new Date(),
endTime: null,
status: 'running',
options: options,
results: [],
summary: null,
coverage: null,
error: null
};
return session;
}
private async discoverTestFiles(options: TestRunOptions): Promise<string[]> {
const pattern = options.testPattern || this.testConfig.testPattern;
const exclude = options.excludePattern || this.testConfig.excludePattern;
// ワークスペース内のテストファイルを検索
const testFiles: string[] = [];
if (TraeAPI.workspace.workspaceFolders) {
for (const folder of TraeAPI.workspace.workspaceFolders) {
const files = await TraeAPI.workspace.findFiles(
new TraeAPI.RelativePattern(folder, pattern),
exclude
);
testFiles.push(...files.map(file => file.fsPath));
}
}
// 特定のファイルが指定されている場合
if (options.files && options.files.length > 0) {
return options.files.filter(file => testFiles.includes(file));
}
return testFiles;
}
private async loadTestSuites(testFiles: string[]): Promise<TestSuite[]> {
const testSuites: TestSuite[] = [];
for (const file of testFiles) {
try {
console.log(`テストファイルを読み込み中: ${file}`);
// ファイルの内容を読み取り
const content = await TraeAPI.workspace.fs.readFile(TraeAPI.Uri.file(file));
const code = content.toString();
// テストフレームワークを検出
const framework = this.detectTestFramework(code);
if (framework) {
// テストスイートを解析
const suite = await framework.parseTestSuite(file, code);
testSuites.push(suite);
this.testSuites.set(suite.id, suite);
} else {
console.warn(`テストフレームワークが検出できませんでした: ${file}`);
}
} catch (error) {
console.error(`テストファイルの読み込みに失敗しました ${file}:`, error);
}
}
return testSuites;
}
private detectTestFramework(code: string): TestFramework | null {
// コードからテストフレームワークを検出
if (code.includes('describe(') || code.includes('it(') || code.includes('test(')) {
if (code.includes('jest') || code.includes('@jest')) {
return this.frameworks.get('jest') || null;
} else if (code.includes('mocha')) {
return this.frameworks.get('mocha') || null;
} else if (code.includes('vitest')) {
return this.frameworks.get('vitest') || null;
}
// デフォルトはJest
return this.frameworks.get('jest') || null;
}
if (code.includes('test(') && code.includes('expect(')) {
return this.frameworks.get('playwright') || null;
}
if (code.includes('cy.') || code.includes('cypress')) {
return this.frameworks.get('cypress') || null;
}
return null;
}
private async executeTests(testSuites: TestSuite[], options: TestRunOptions): Promise<TestResult[]> {
const results: TestResult[] = [];
if (options.parallel && this.testConfig.parallel) {
// 並列実行
console.log('テストを並列実行中...');
const promises = testSuites.map(suite => this.executeTestSuite(suite, options));
const suiteResults = await Promise.all(promises);
results.push(...suiteResults.flat());
} else {
// 順次実行
console.log('テストを順次実行中...');
for (const suite of testSuites) {
const suiteResults = await this.executeTestSuite(suite, options);
results.push(...suiteResults);
}
}
return results;
}
private async executeTestSuite(suite: TestSuite, options: TestRunOptions): Promise<TestResult[]> {
const results: TestResult[] = [];
try {
console.log(`テストスイートを実行中: ${suite.name}`);
// セットアップを実行
await this.runSetupHooks(suite);
// テストケースを実行
for (const testCase of suite.tests) {
const result = await this.executeTestCase(testCase, suite, options);
results.push(result);
// テストリスナーに通知
this.notifyTestCaseComplete(result);
}
// ティアダウンを実行
await this.runTeardownHooks(suite);
} catch (error) {
console.error(`テストスイート ${suite.name} でエラーが発生しました:`, error);
// スイート全体が失敗した場合
const errorResult: TestResult = {
id: `${suite.id}-error`,
testCase: {
id: 'suite-error',
name: 'Suite Error',
description: `テストスイート ${suite.name} でエラーが発生しました`,
timeout: 0,
retries: 0
},
suite: suite,
status: 'failed',
startTime: new Date(),
endTime: new Date(),
duration: 0,
error: error.message,
stackTrace: error.stack,
output: null,
coverage: null
};
results.push(errorResult);
}
return results;
}
private async executeTestCase(testCase: TestCase, suite: TestSuite, options: TestRunOptions): Promise<TestResult> {
const startTime = new Date();
let status: TestStatus = 'running';
let error: string | null = null;
let stackTrace: string | null = null;
let output: string | null = null;
let coverage: CoverageInfo | null = null;
try {
console.log(` テストケースを実行中: ${testCase.name}`);
// テストリスナーに通知
this.notifyTestCaseStart(testCase, suite);
// タイムアウト設定
const timeout = testCase.timeout || options.timeout || this.testConfig.timeout;
// テストを実行(リトライ機能付き)
const maxRetries = testCase.retries || options.retries || this.testConfig.retries;
let attempt = 0;
while (attempt <= maxRetries) {
try {
// beforeEachフックを実行
await this.runBeforeEachHooks(suite, testCase);
// テスト本体を実行
await this.runTestWithTimeout(testCase, timeout);
// afterEachフックを実行
await this.runAfterEachHooks(suite, testCase);
status = 'passed';
break;
} catch (testError) {
attempt++;
if (attempt > maxRetries) {
throw testError;
}
console.warn(`テスト ${testCase.name} が失敗しました(試行 ${attempt}/${maxRetries + 1}):`, testError.message);
// リトライ前に少し待機
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// カバレッジ情報を収集
if (this.testConfig.coverage) {
coverage = await this.collectCoverage(testCase, suite);
}
} catch (testError) {
status = 'failed';
error = testError.message;
stackTrace = testError.stack;
console.error(`テスト ${testCase.name} が失敗しました:`, testError.message);
}
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
const result: TestResult = {
id: `${suite.id}-${testCase.id}`,
testCase: testCase,
suite: suite,
status: status,
startTime: startTime,
endTime: endTime,
duration: duration,
error: error,
stackTrace: stackTrace,
output: output,
coverage: coverage
};
// 結果を保存
this.testResults.set(result.id, result);
return result;
}
private async runTestWithTimeout(testCase: TestCase, timeout: number): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`テストがタイムアウトしました(${timeout}ms)`));
}, timeout);
try {
// テスト関数を実行
const testPromise = testCase.fn();
if (testPromise && typeof testPromise.then === 'function') {
// 非同期テスト
testPromise
.then(() => {
clearTimeout(timer);
resolve();
})
.catch((error) => {
clearTimeout(timer);
reject(error);
});
} else {
// 同期テスト
clearTimeout(timer);
resolve();
}
} catch (error) {
clearTimeout(timer);
reject(error);
}
});
}
// テストカバレッジ
async generateCoverageReport(session: TestSession): Promise<CoverageReport> {
console.log('カバレッジレポートを生成中...');
const coverageData: CoverageInfo[] = [];
// 各テスト結果からカバレッジ情報を収集
for (const result of session.results) {
if (result.coverage) {
coverageData.push(result.coverage);
}
}
// カバレッジデータを統合
const aggregatedCoverage = this.aggregateCoverage(coverageData);
// レポートを生成
const report: CoverageReport = {
id: `coverage-${session.id}`,
sessionId: session.id,
timestamp: new Date(),
summary: {
lines: aggregatedCoverage.lines,
functions: aggregatedCoverage.functions,
branches: aggregatedCoverage.branches,
statements: aggregatedCoverage.statements
},
files: aggregatedCoverage.files,
thresholds: {
lines: this.testConfig.coverageThreshold,
functions: this.testConfig.coverageThreshold,
branches: this.testConfig.coverageThreshold,
statements: this.testConfig.coverageThreshold
},
passed: this.checkCoverageThresholds(aggregatedCoverage)
};
// レポートファイルを生成
await this.saveCoverageReport(report);
return report;
}
private aggregateCoverage(coverageData: CoverageInfo[]): AggregatedCoverage {
const fileMap = new Map<string, FileCoverage>();
// ファイルごとにカバレッジを集計
for (const coverage of coverageData) {
for (const file of coverage.files) {
const existing = fileMap.get(file.path);
if (existing) {
// 既存のカバレッジとマージ
existing.lines.covered += file.lines.covered;
existing.lines.total = Math.max(existing.lines.total, file.lines.total);
existing.functions.covered += file.functions.covered;
existing.functions.total = Math.max(existing.functions.total, file.functions.total);
existing.branches.covered += file.branches.covered;
existing.branches.total = Math.max(existing.branches.total, file.branches.total);
existing.statements.covered += file.statements.covered;
existing.statements.total = Math.max(existing.statements.total, file.statements.total);
} else {
fileMap.set(file.path, { ...file });
}
}
}
const files = Array.from(fileMap.values());
// 全体のカバレッジを計算
const totalLines = files.reduce((sum, file) => sum + file.lines.total, 0);
const coveredLines = files.reduce((sum, file) => sum + file.lines.covered, 0);
const totalFunctions = files.reduce((sum, file) => sum + file.functions.total, 0);
const coveredFunctions = files.reduce((sum, file) => sum + file.functions.covered, 0);
const totalBranches = files.reduce((sum, file) => sum + file.branches.total, 0);
const coveredBranches = files.reduce((sum, file) => sum + file.branches.covered, 0);
const totalStatements = files.reduce((sum, file) => sum + file.statements.total, 0);
const coveredStatements = files.reduce((sum, file) => sum + file.statements.covered, 0);
return {
lines: {
total: totalLines,
covered: coveredLines,
percentage: totalLines > 0 ? (coveredLines / totalLines) * 100 : 0
},
functions: {
total: totalFunctions,
covered: coveredFunctions,
percentage: totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0
},
branches: {
total: totalBranches,
covered: coveredBranches,
percentage: totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0
},
statements: {
total: totalStatements,
covered: coveredStatements,
percentage: totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0
},
files: files
};
}
private checkCoverageThresholds(coverage: AggregatedCoverage): boolean {
const threshold = this.testConfig.coverageThreshold;
return (
coverage.lines.percentage >= threshold &&
coverage.functions.percentage >= threshold &&
coverage.branches.percentage >= threshold &&
coverage.statements.percentage >= threshold
);
}
private async saveCoverageReport(report: CoverageReport): Promise<void> {
try {
// HTMLレポートを生成
const htmlReport = this.generateHTMLCoverageReport(report);
// レポートディレクトリを作成
const reportDir = TraeAPI.Uri.file(path.join(TraeAPI.workspace.rootPath || '', 'coverage'));
await TraeAPI.workspace.fs.createDirectory(reportDir);
// HTMLファイルを保存
const htmlFile = TraeAPI.Uri.file(path.join(reportDir.fsPath, 'index.html'));
await TraeAPI.workspace.fs.writeFile(htmlFile, Buffer.from(htmlReport));
// JSONファイルを保存
const jsonFile = TraeAPI.Uri.file(path.join(reportDir.fsPath, 'coverage.json'));
await TraeAPI.workspace.fs.writeFile(jsonFile, Buffer.from(JSON.stringify(report, null, 2)));
console.log(`カバレッジレポートが保存されました: ${reportDir.fsPath}`);
// レポートを開くかユーザーに確認
const action = await TraeAPI.window.showInformationMessage(
'カバレッジレポートが生成されました。',
'レポートを開く',
'閉じる'
);
if (action === 'レポートを開く') {
await TraeAPI.env.openExternal(htmlFile);
}
} catch (error) {
console.error('カバレッジレポートの保存に失敗しました:', error);
}
}
private generateHTMLCoverageReport(report: CoverageReport): string {
return `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>テストカバレッジレポート</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; }
.metric { background: white; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
.metric-value { font-size: 2em; font-weight: bold; margin-bottom: 5px; }
.metric-label { color: #666; }
.passed { color: #28a745; }
.failed { color: #dc3545; }
.files { margin-top: 30px; }
.file { background: white; margin-bottom: 10px; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.file-name { font-weight: bold; margin-bottom: 10px; }
.file-metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.file-metric { text-align: center; }
.progress-bar { width: 100%; height: 20px; background: #e9ecef; border-radius: 10px; overflow: hidden; }
.progress-fill { height: 100%; background: #28a745; transition: width 0.3s ease; }
</style>
</head>
<body>
<div class="header">
<h1>テストカバレッジレポート</h1>
<p>生成日時: ${report.timestamp.toLocaleString('ja-JP')}</p>
<p>セッションID: ${report.sessionId}</p>
</div>
<div class="summary">
<div class="metric">
<div class="metric-value ${report.summary.lines.percentage >= report.thresholds.lines ? 'passed' : 'failed'}">
${report.summary.lines.percentage.toFixed(1)}%
</div>
<div class="metric-label">行カバレッジ</div>
<div>${report.summary.lines.covered}/${report.summary.lines.total}</div>
</div>
<div class="metric">
<div class="metric-value ${report.summary.functions.percentage >= report.thresholds.functions ? 'passed' : 'failed'}">
${report.summary.functions.percentage.toFixed(1)}%
</div>
<div class="metric-label">関数カバレッジ</div>
<div>${report.summary.functions.covered}/${report.summary.functions.total}</div>
</div>
<div class="metric">
<div class="metric-value ${report.summary.branches.percentage >= report.thresholds.branches ? 'passed' : 'failed'}">
${report.summary.branches.percentage.toFixed(1)}%
</div>
<div class="metric-label">分岐カバレッジ</div>
<div>${report.summary.branches.covered}/${report.summary.branches.total}</div>
</div>
<div class="metric">
<div class="metric-value ${report.summary.statements.percentage >= report.thresholds.statements ? 'passed' : 'failed'}">
${report.summary.statements.percentage.toFixed(1)}%
</div>
<div class="metric-label">文カバレッジ</div>
<div>${report.summary.statements.covered}/${report.summary.statements.total}</div>
</div>
</div>
<div class="files">
<h2>ファイル別カバレッジ</h2>
${report.files.map(file => `
<div class="file">
<div class="file-name">${file.path}</div>
<div class="file-metrics">
<div class="file-metric">
<div>行: ${file.lines.percentage.toFixed(1)}%</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${file.lines.percentage}%"></div>
</div>
</div>
<div class="file-metric">
<div>関数: ${file.functions.percentage.toFixed(1)}%</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${file.functions.percentage}%"></div>
</div>
</div>
<div class="file-metric">
<div>分岐: ${file.branches.percentage.toFixed(1)}%</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${file.branches.percentage}%"></div>
</div>
</div>
<div class="file-metric">
<div>文: ${file.statements.percentage.toFixed(1)}%</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${file.statements.percentage}%"></div>
</div>
</div>
</div>
</div>
`).join('')}
</div>
</body>
</html>
`;
}
// テストデバッグ
async debugTest(testCase: TestCase, suite: TestSuite): Promise<void> {
console.log(`テストをデバッグ中: ${testCase.name}`);
try {
// デバッグセッションを開始
const debugConfig = {
type: 'node',
request: 'launch',
name: `Debug Test: ${testCase.name}`,
program: suite.file,
args: ['--testNamePattern', testCase.name],
console: 'integratedTerminal',
internalConsoleOptions: 'neverOpen',
skipFiles: ['<node_internals>/**']
};
await TraeAPI.debug.startDebugging(undefined, debugConfig);
} catch (error) {
console.error('テストのデバッグに失敗しました:', error);
TraeAPI.window.showErrorMessage(`テストのデバッグに失敗しました: ${error.message}`);
}
}
// テストウォッチモード
async startWatchMode(options: TestWatchOptions = {}): Promise<TestWatcher> {
console.log('テストウォッチモードを開始中...');
const watcher = new TestWatcher(this, options);
await watcher.start();
return watcher;
}
// ユーティリティメソッド
getTestSuites(): TestSuite[] {
return Array.from(this.testSuites.values());
}
getTestResults(): TestResult[] {
return Array.from(this.testResults.values());
}
getCurrentSession(): TestSession | null {
return this.currentSession;
}
isTestRunning(): boolean {
return this.isRunning;
}
// イベント処理
onTestStart(listener: (session: TestSession) => void): TraeAPI.Disposable {
const testListener: TestListener = {
onTestStart: listener,
onTestComplete: () => {},
onTestCaseStart: () => {},
onTestCaseComplete: () => {},
onTestError: () => {}
};
this.testListeners.push(testListener);
return {
dispose: () => {
const index = this.testListeners.indexOf(testListener);
if (index >= 0) {
this.testListeners.splice(index, 1);
}
}
};
}
onTestComplete(listener: (session: TestSession) => void): TraeAPI.Disposable {
const testListener: TestListener = {
onTestStart: () => {},
onTestComplete: listener,
onTestCaseStart: () => {},
onTestCaseComplete: () => {},
onTestError: () => {}
};
this.testListeners.push(testListener);
return {
dispose: () => {
const index = this.testListeners.indexOf(testListener);
if (index >= 0) {
this.testListeners.splice(index, 1);
}
}
};
}
onTestCaseComplete(listener: (result: TestResult) => void): TraeAPI.Disposable {
const testListener: TestListener = {
onTestStart: () => {},
onTestComplete: () => {},
onTestCaseStart: () => {},
onTestCaseComplete: listener,
onTestError: () => {}
};
this.testListeners.push(testListener);
return {
dispose: () => {
const index = this.testListeners.indexOf(testListener);
if (index >= 0) {
this.testListeners.splice(index, 1);
}
}
};
}
private notifyTestStart(session: TestSession): void {
this.testListeners.forEach(listener => {
try {
listener.onTestStart(session);
} catch (error) {
console.error('テスト開始リスナーでエラーが発生しました:', error);
}
});
}
private notifyTestComplete(session: TestSession): void {
this.testListeners.forEach(listener => {
try {
listener.onTestComplete(session);
} catch (error) {
console.error('テスト完了リスナーでエラーが発生しました:', error);
}
});
}
private notifyTestCaseStart(testCase: TestCase, suite: TestSuite): void {
this.testListeners.forEach(listener => {
try {
listener.onTestCaseStart(testCase, suite);
} catch (error) {
console.error('テストケース開始リスナーでエラーが発生しました:', error);
}
});
}
private notifyTestCaseComplete(result: TestResult): void {
this.testListeners.forEach(listener => {
try {
listener.onTestCaseComplete(result);
} catch (error) {
console.error('テストケース完了リスナーでエラーが発生しました:', error);
}
});
}
private notifyTestError(session: TestSession, error: Error): void {
this.testListeners.forEach(listener => {
try {
listener.onTestError(session, error);
} catch (listenerError) {
console.error('テストエラーリスナーでエラーが発生しました:', listenerError);
}
});
}
// ヘルパーメソッド
private generateTestSummary(results: TestResult[]): TestSummary {
const passed = results.filter(r => r.status === 'passed').length;
const failed = results.filter(r => r.status === 'failed').length;
const skipped = results.filter(r => r.status === 'skipped').length;
const total = results.length;
const duration = results.reduce((sum, r) => sum + r.duration, 0);
return {
total: total,
passed: passed,
failed: failed,
skipped: skipped,
duration: duration,
passRate: total > 0 ? (passed / total) * 100 : 0
};
}
private async runSetupHooks(suite: TestSuite): Promise<void> {
for (const hook of suite.beforeAllHooks || []) {
await hook();
}
}
private async runTeardownHooks(suite: TestSuite): Promise<void> {
for (const hook of suite.afterAllHooks || []) {
await hook();
}
}
private async runBeforeEachHooks(suite: TestSuite, testCase: TestCase): Promise<void> {
for (const hook of suite.beforeEachHooks || []) {
await hook();
}
}
private async runAfterEachHooks(suite: TestSuite, testCase: TestCase): Promise<void> {
for (const hook of suite.afterEachHooks || []) {
await hook();
}
}
private async collectCoverage(testCase: TestCase, suite: TestSuite): Promise<CoverageInfo | null> {
// カバレッジ収集の実装
// 実際の実装では、テストフレームワークのカバレッジツールと統合
return null;
}
// グローバル関数の実装
private createExpectFunction() {
return (actual: any) => ({
toBe: (expected: any) => {
if (actual !== expected) {
throw new Error(`期待値: ${expected}, 実際の値: ${actual}`);
}
},
toEqual: (expected: any) => {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`期待値: ${JSON.stringify(expected)}, 実際の値: ${JSON.stringify(actual)}`);
}
},
toBeTruthy: () => {
if (!actual) {
throw new Error(`期待値: truthy, 実際の値: ${actual}`);
}
},
toBeFalsy: () => {
if (actual) {
throw new Error(`期待値: falsy, 実際の値: ${actual}`);
}
}
});
}
private createDescribeFunction() {
return (name: string, fn: () => void) => {
// describe関数の実装
console.log(`テストスイート: ${name}`);
fn();
};
}
private createItFunction() {
return (name: string, fn: () => void | Promise<void>) => {
// it関数の実装
console.log(` テストケース: ${name}`);
return fn();
};
}
private createBeforeEachFunction() {
return (fn: () => void | Promise<void>) => {
// beforeEach関数の実装
return fn();
};
}
private createAfterEachFunction() {
return (fn: () => void | Promise<void>) => {
// afterEach関数の実装
return fn();
};
}
dispose(): void {
this.testSuites.clear();
this.testResults.clear();
this.testListeners.length = 0;
this.isRunning = false;
this.currentSession = null;
}
}
// テストウォッチャー
class TestWatcher {
private testRunner: TestRunner;
private options: TestWatchOptions;
private watchers: TraeAPI.FileSystemWatcher[] = [];
private isWatching: boolean = false;
constructor(testRunner: TestRunner, options: TestWatchOptions) {
this.testRunner = testRunner;
this.options = options;
}
async start(): Promise<void> {
if (this.isWatching) {
return;
}
this.isWatching = true;
console.log('テストウォッチモードを開始しました');
// テストファイルの変更を監視
const testWatcher = TraeAPI.workspace.createFileSystemWatcher('**/*.test.{js,ts}');
testWatcher.onDidChange(uri => this.handleFileChange(uri));
testWatcher.onDidCreate(uri => this.handleFileChange(uri));
testWatcher.onDidDelete(uri => this.handleFileDelete(uri));
this.watchers.push(testWatcher);
// ソースファイルの変更を監視
const sourceWatcher = TraeAPI.workspace.createFileSystemWatcher('**/*.{js,ts}');
sourceWatcher.onDidChange(uri => this.handleSourceFileChange(uri));
this.watchers.push(sourceWatcher);
// 初回テスト実行
if (this.options.runOnStart !== false) {
await this.runTests();
}
}
async stop(): Promise<void> {
if (!this.isWatching) {
return;
}
this.isWatching = false;
console.log('テストウォッチモードを停止しました');
// ウォッチャーを停止
this.watchers.forEach(watcher => watcher.dispose());
this.watchers = [];
}
private async handleFileChange(uri: TraeAPI.Uri): Promise<void> {
console.log(`テストファイルが変更されました: ${uri.fsPath}`);
if (this.options.debounceMs) {
// デバウンス処理
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.runTests([uri.fsPath]);
}, this.options.debounceMs);
} else {
await this.runTests([uri.fsPath]);
}
}
private async handleFileDelete(uri: TraeAPI.Uri): Promise<void> {
console.log(`テストファイルが削除されました: ${uri.fsPath}`);
// 削除されたファイルに関連するテスト結果をクリア
}
private async handleSourceFileChange(uri: TraeAPI.Uri): Promise<void> {
if (uri.fsPath.includes('.test.') || uri.fsPath.includes('.spec.')) {
return; // テストファイルは別途処理
}
console.log(`ソースファイルが変更されました: ${uri.fsPath}`);
// 関連するテストファイルを検索
const relatedTests = await this.findRelatedTests(uri.fsPath);
if (relatedTests.length > 0) {
await this.runTests(relatedTests);
}
}
private async findRelatedTests(sourceFile: string): Promise<string[]> {
// ソースファイルに関連するテストファイルを検索
const relatedTests: string[] = [];
const baseName = path.basename(sourceFile, path.extname(sourceFile));
const dirName = path.dirname(sourceFile);
// 同じディレクトリ内のテストファイルを検索
const testPatterns = [
`${baseName}.test.js`,
`${baseName}.test.ts`,
`${baseName}.spec.js`,
`${baseName}.spec.ts`
];
for (const pattern of testPatterns) {
const testFile = path.join(dirName, pattern);
try {
await TraeAPI.workspace.fs.stat(TraeAPI.Uri.file(testFile));
relatedTests.push(testFile);
} catch {
// ファイルが存在しない
}
}
return relatedTests;
}
private async runTests(files?: string[]): Promise<void> {
try {
const options: TestRunOptions = {
files: files,
...this.options.testOptions
};
await this.testRunner.runTests(options);
} catch (error) {
console.error('ウォッチモードでのテスト実行に失敗しました:', error);
}
}
private debounceTimer: NodeJS.Timeout | null = null;
}
// インターフェース定義
interface TestRunOptions {
files?: string[];
testPattern?: string;
excludePattern?: string;
timeout?: number;
retries?: number;
parallel?: boolean;
coverage?: boolean;
}
interface TestWatchOptions {
runOnStart?: boolean;
debounceMs?: number;
testOptions?: TestRunOptions;
}
interface TestSession {
id: string;
startTime: Date;
endTime: Date | null;
status: 'running' | 'passed' | 'failed' | 'error';
options: TestRunOptions;
results: TestResult[];
summary: TestSummary | null;
coverage: CoverageReport | null;
error: string | null;
}
interface TestSuite {
id: string;
name: string;
description: string;
file: string;
tests: TestCase[];
beforeAllHooks?: (() => void | Promise<void>)[];
afterAllHooks?: (() => void | Promise<void>)[];
beforeEachHooks?: (() => void | Promise<void>)[];
afterEachHooks?: (() => void | Promise<void>)[];
}
interface TestCase {
id: string;
name: string;
description: string;
fn: () => void | Promise<void>;
timeout?: number;
retries?: number;
skip?: boolean;
only?: boolean;
}
interface TestResult {
id: string;
testCase: TestCase;
suite: TestSuite;
status: TestStatus;
startTime: Date;
endTime: Date;
duration: number;
error: string | null;
stackTrace: string | null;
output: string | null;
coverage: CoverageInfo | null;
}
type TestStatus = 'running' | 'passed' | 'failed' | 'skipped';
interface TestSummary {
total: number;
passed: number;
failed: number;
skipped: number;
duration: number;
passRate: number;
}
interface TestListener {
onTestStart: (session: TestSession) => void;
onTestComplete: (session: TestSession) => void;
onTestCaseStart: (testCase: TestCase, suite: TestSuite) => void;
onTestCaseComplete: (result: TestResult) => void;
onTestError: (session: TestSession, error: Error) => void;
}
interface CoverageInfo {
files: FileCoverage[];
}
interface FileCoverage {
path: string;
lines: CoverageMetric;
functions: CoverageMetric;
branches: CoverageMetric;
statements: CoverageMetric;
}
interface CoverageMetric {
total: number;
covered: number;
percentage: number;
}
interface CoverageReport {
id: string;
sessionId: string;
timestamp: Date;
summary: {
lines: CoverageMetric;
functions: CoverageMetric;
branches: CoverageMetric;
statements: CoverageMetric;
};
files: FileCoverage[];
thresholds: {
lines: number;
functions: number;
branches: number;
statements: number;
};
passed: boolean;
}
interface AggregatedCoverage {
lines: CoverageMetric;
functions: CoverageMetric;
branches: CoverageMetric;
statements: CoverageMetric;
files: FileCoverage[];
}
interface TestFramework {
parseTestSuite(file: string, code: string): Promise<TestSuite>;
runTest(testCase: TestCase): Promise<TestResult>;
}
// テストフレームワークの実装例
class JestFramework implements TestFramework {
async parseTestSuite(file: string, code: string): Promise<TestSuite> {
// Jestテストファイルを解析してTestSuiteを生成
const suite: TestSuite = {
id: `jest-${path.basename(file)}-${Date.now()}`,
name: path.basename(file),
description: `Jest test suite for ${file}`,
file: file,
tests: []
};
// コードを解析してテストケースを抽出
// 実際の実装では、ASTパーサーを使用してより正確に解析
const testMatches = code.match(/(?:it|test)\s*\(\s*['"`]([^'"`]+)['"`]/g);
if (testMatches) {
testMatches.forEach((match, index) => {
const nameMatch = match.match(/['"`]([^'"`]+)['"`]/);
if (nameMatch) {
const testCase: TestCase = {
id: `test-${index}`,
name: nameMatch[1],
description: nameMatch[1],
fn: () => {
// 実際のテスト実行はJestランナーに委譲
console.log(`Jestテストを実行: ${nameMatch[1]}`);
}
};
suite.tests.push(testCase);
}
});
}
return suite;
}
async runTest(testCase: TestCase): Promise<TestResult> {
// Jest固有のテスト実行ロジック
const startTime = new Date();
try {
await testCase.fn();
return {
id: `jest-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'passed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: null,
stackTrace: null,
output: null,
coverage: null
};
} catch (error) {
return {
id: `jest-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'failed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: error.message,
stackTrace: error.stack,
output: null,
coverage: null
};
}
}
}
class MochaFramework implements TestFramework {
async parseTestSuite(file: string, code: string): Promise<TestSuite> {
// Mochaテストファイルを解析
const suite: TestSuite = {
id: `mocha-${path.basename(file)}-${Date.now()}`,
name: path.basename(file),
description: `Mocha test suite for ${file}`,
file: file,
tests: []
};
// Mocha固有の解析ロジック
return suite;
}
async runTest(testCase: TestCase): Promise<TestResult> {
// Mocha固有のテスト実行ロジック
const startTime = new Date();
try {
await testCase.fn();
return {
id: `mocha-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'passed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: null,
stackTrace: null,
output: null,
coverage: null
};
} catch (error) {
return {
id: `mocha-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'failed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: error.message,
stackTrace: error.stack,
output: null,
coverage: null
};
}
}
}
class VitestFramework implements TestFramework {
async parseTestSuite(file: string, code: string): Promise<TestSuite> {
// Vitestテストファイルを解析
const suite: TestSuite = {
id: `vitest-${path.basename(file)}-${Date.now()}`,
name: path.basename(file),
description: `Vitest test suite for ${file}`,
file: file,
tests: []
};
// Vitest固有の解析ロジック
return suite;
}
async runTest(testCase: TestCase): Promise<TestResult> {
// Vitest固有のテスト実行ロジック
const startTime = new Date();
try {
await testCase.fn();
return {
id: `vitest-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'passed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: null,
stackTrace: null,
output: null,
coverage: null
};
} catch (error) {
return {
id: `vitest-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'failed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: error.message,
stackTrace: error.stack,
output: null,
coverage: null
};
}
}
}
class PlaywrightFramework implements TestFramework {
async parseTestSuite(file: string, code: string): Promise<TestSuite> {
// Playwrightテストファイルを解析
const suite: TestSuite = {
id: `playwright-${path.basename(file)}-${Date.now()}`,
name: path.basename(file),
description: `Playwright test suite for ${file}`,
file: file,
tests: []
};
// Playwright固有の解析ロジック
return suite;
}
async runTest(testCase: TestCase): Promise<TestResult> {
// Playwright固有のテスト実行ロジック
const startTime = new Date();
try {
await testCase.fn();
return {
id: `playwright-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'passed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: null,
stackTrace: null,
output: null,
coverage: null
};
} catch (error) {
return {
id: `playwright-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'failed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: error.message,
stackTrace: error.stack,
output: null,
coverage: null
};
}
}
}
class CypressFramework implements TestFramework {
async parseTestSuite(file: string, code: string): Promise<TestSuite> {
// Cypressテストファイルを解析
const suite: TestSuite = {
id: `cypress-${path.basename(file)}-${Date.now()}`,
name: path.basename(file),
description: `Cypress test suite for ${file}`,
file: file,
tests: []
};
// Cypress固有の解析ロジック
return suite;
}
async runTest(testCase: TestCase): Promise<TestResult> {
// Cypress固有のテスト実行ロジック
const startTime = new Date();
try {
await testCase.fn();
return {
id: `cypress-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'passed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: null,
stackTrace: null,
output: null,
coverage: null
};
} catch (error) {
return {
id: `cypress-${testCase.id}`,
testCase: testCase,
suite: null as any,
status: 'failed',
startTime: startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
error: error.message,
stackTrace: error.stack,
output: null,
coverage: null
};
}
}
}
// テストランナーを初期化
const testRunner = new TestRunner();テストユーティリティ
typescript
// テストユーティリティクラス
class TestUtils {
// モックデータ生成
static generateMockData<T>(template: Partial<T>, count: number = 1): T[] {
const mockData: T[] = [];
for (let i = 0; i < count; i++) {
const mock = { ...template } as T;
// 動的プロパティの生成
Object.keys(mock).forEach(key => {
const value = (mock as any)[key];
if (typeof value === 'string' && value.includes('{{index}}')) {
(mock as any)[key] = value.replace('{{index}}', i.toString());
}
if (typeof value === 'string' && value.includes('{{random}}')) {
(mock as any)[key] = value.replace('{{random}}', Math.random().toString(36).substring(7));
}
if (typeof value === 'string' && value.includes('{{timestamp}}')) {
(mock as any)[key] = value.replace('{{timestamp}}', Date.now().toString());
}
});
mockData.push(mock);
}
return mockData;
}
// APIモック
static createApiMock(baseUrl: string, responses: Record<string, any>): void {
// fetch APIをモック
const originalFetch = global.fetch;
global.fetch = jest.fn((url: string, options?: RequestInit) => {
const endpoint = url.replace(baseUrl, '');
const method = options?.method || 'GET';
const key = `${method} ${endpoint}`;
if (responses[key]) {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(responses[key]),
text: () => Promise.resolve(JSON.stringify(responses[key]))
} as Response);
}
return Promise.reject(new Error(`モックされていないエンドポイント: ${key}`));
});
// テスト後にクリーンアップ
afterEach(() => {
global.fetch = originalFetch;
});
}
// データベースモック
static createDatabaseMock(initialData: Record<string, any[]> = {}): DatabaseMock {
return new DatabaseMock(initialData);
}
// ファイルシステムモック
static createFileSystemMock(files: Record<string, string> = {}): FileSystemMock {
return new FileSystemMock(files);
}
// 時間モック
static mockTime(fixedTime: Date): void {
const originalDate = Date;
global.Date = class extends Date {
constructor(...args: any[]) {
if (args.length === 0) {
super(fixedTime);
} else {
super(...args);
}
}
static now(): number {
return fixedTime.getTime();
}
} as any;
afterEach(() => {
global.Date = originalDate;
});
}
// 非同期テストヘルパー
static async waitFor(condition: () => boolean, timeout: number = 5000): Promise<void> {
const startTime = Date.now();
while (!condition()) {
if (Date.now() - startTime > timeout) {
throw new Error(`条件が満たされませんでした(タイムアウト: ${timeout}ms)`);
}
await new Promise(resolve => setTimeout(resolve, 10));
}
}
// イベントテストヘルパー
static async waitForEvent<T>(
emitter: { on: (event: string, listener: (data: T) => void) => void },
eventName: string,
timeout: number = 5000
): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`イベント ${eventName} がタイムアウトしました(${timeout}ms)`));
}, timeout);
emitter.on(eventName, (data: T) => {
clearTimeout(timer);
resolve(data);
});
});
}
// スナップショットテスト
static matchSnapshot(actual: any, snapshotName: string): void {
const snapshotPath = path.join(__dirname, '__snapshots__', `${snapshotName}.json`);
try {
const expectedSnapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'));
expect(actual).toEqual(expectedSnapshot);
} catch (error) {
// スナップショットファイルが存在しない場合は作成
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
fs.writeFileSync(snapshotPath, JSON.stringify(actual, null, 2));
console.log(`スナップショットを作成しました: ${snapshotPath}`);
}
}
}
// データベースモッククラス
class DatabaseMock {
private data: Record<string, any[]>;
constructor(initialData: Record<string, any[]>) {
this.data = { ...initialData };
}
find(table: string, query: any = {}): any[] {
const records = this.data[table] || [];
if (Object.keys(query).length === 0) {
return [...records];
}
return records.filter(record => {
return Object.keys(query).every(key => {
const queryValue = query[key];
const recordValue = record[key];
if (typeof queryValue === 'object' && queryValue !== null) {
// 複雑なクエリ($gt, $lt, $in など)
return this.evaluateQuery(recordValue, queryValue);
}
return recordValue === queryValue;
});
});
}
findOne(table: string, query: any = {}): any | null {
const results = this.find(table, query);
return results.length > 0 ? results[0] : null;
}
insert(table: string, record: any): any {
if (!this.data[table]) {
this.data[table] = [];
}
const newRecord = {
id: Date.now() + Math.random(),
...record,
createdAt: new Date(),
updatedAt: new Date()
};
this.data[table].push(newRecord);
return newRecord;
}
update(table: string, query: any, update: any): number {
const records = this.find(table, query);
let updatedCount = 0;
records.forEach(record => {
Object.assign(record, update, { updatedAt: new Date() });
updatedCount++;
});
return updatedCount;
}
delete(table: string, query: any): number {
const records = this.data[table] || [];
const initialLength = records.length;
this.data[table] = records.filter(record => {
return !Object.keys(query).every(key => record[key] === query[key]);
});
return initialLength - this.data[table].length;
}
clear(table?: string): void {
if (table) {
this.data[table] = [];
} else {
this.data = {};
}
}
private evaluateQuery(value: any, query: any): boolean {
if (query.$gt !== undefined) {
return value > query.$gt;
}
if (query.$gte !== undefined) {
return value >= query.$gte;
}
if (query.$lt !== undefined) {
return value < query.$lt;
}
if (query.$lte !== undefined) {
return value <= query.$lte;
}
if (query.$in !== undefined) {
return query.$in.includes(value);
}
if (query.$nin !== undefined) {
return !query.$nin.includes(value);
}
if (query.$ne !== undefined) {
return value !== query.$ne;
}
return false;
}
}
// ファイルシステムモッククラス
class FileSystemMock {
private files: Map<string, string>;
constructor(files: Record<string, string>) {
this.files = new Map(Object.entries(files));
}
readFile(path: string): string {
if (!this.files.has(path)) {
throw new Error(`ファイルが見つかりません: ${path}`);
}
return this.files.get(path)!;
}
writeFile(path: string, content: string): void {
this.files.set(path, content);
}
exists(path: string): boolean {
return this.files.has(path);
}
delete(path: string): boolean {
return this.files.delete(path);
}
list(): string[] {
return Array.from(this.files.keys());
}
clear(): void {
this.files.clear();
}
}パフォーマンステスト
typescript
// パフォーマンステストユーティリティ
class PerformanceTestUtils {
// 実行時間測定
static async measureExecutionTime<T>(
fn: () => Promise<T> | T,
iterations: number = 1
): Promise<{ result: T; averageTime: number; totalTime: number; iterations: number }> {
const times: number[] = [];
let result: T;
for (let i = 0; i < iterations; i++) {
const startTime = performance.now();
result = await fn();
const endTime = performance.now();
times.push(endTime - startTime);
}
const totalTime = times.reduce((sum, time) => sum + time, 0);
const averageTime = totalTime / iterations;
return {
result: result!,
averageTime,
totalTime,
iterations
};
}
// メモリ使用量測定
static measureMemoryUsage<T>(fn: () => T): { result: T; memoryUsed: number } {
const initialMemory = process.memoryUsage().heapUsed;
const result = fn();
const finalMemory = process.memoryUsage().heapUsed;
const memoryUsed = finalMemory - initialMemory;
return { result, memoryUsed };
}
// ベンチマーク実行
static async runBenchmark(
name: string,
testCases: Array<{ name: string; fn: () => Promise<any> | any }>,
iterations: number = 100
): Promise<BenchmarkResult[]> {
console.log(`ベンチマーク開始: ${name}`);
const results: BenchmarkResult[] = [];
for (const testCase of testCases) {
console.log(` 実行中: ${testCase.name}`);
const measurement = await this.measureExecutionTime(testCase.fn, iterations);
const result: BenchmarkResult = {
name: testCase.name,
averageTime: measurement.averageTime,
totalTime: measurement.totalTime,
iterations: measurement.iterations,
opsPerSecond: 1000 / measurement.averageTime
};
results.push(result);
console.log(` 平均時間: ${result.averageTime.toFixed(2)}ms`);
console.log(` 秒間実行回数: ${result.opsPerSecond.toFixed(2)} ops/sec`);
}
// 結果をソート(高速順)
results.sort((a, b) => a.averageTime - b.averageTime);
console.log(`\nベンチマーク結果 (${name}):`);
results.forEach((result, index) => {
const rank = index + 1;
const speedRatio = index === 0 ? 1 : result.averageTime / results[0].averageTime;
console.log(`${rank}. ${result.name}: ${result.averageTime.toFixed(2)}ms (${speedRatio.toFixed(2)}x)`);
});
return results;
}
// 負荷テスト
static async runLoadTest(
fn: () => Promise<any> | any,
options: LoadTestOptions
): Promise<LoadTestResult> {
const { concurrency = 10, duration = 10000, rampUp = 1000 } = options;
console.log(`負荷テスト開始: 同時実行数=${concurrency}, 実行時間=${duration}ms`);
const results: Array<{ success: boolean; time: number; error?: string }> = [];
const startTime = Date.now();
let activeRequests = 0;
let completedRequests = 0;
let errors = 0;
// ランプアップ期間中に徐々に負荷を増加
const rampUpInterval = rampUp / concurrency;
const promises: Promise<void>[] = [];
for (let i = 0; i < concurrency; i++) {
const delay = i * rampUpInterval;
const promise = new Promise<void>((resolve) => {
setTimeout(async () => {
while (Date.now() - startTime < duration) {
activeRequests++;
const requestStart = performance.now();
try {
await fn();
const requestTime = performance.now() - requestStart;
results.push({ success: true, time: requestTime });
completedRequests++;
} catch (error) {
const requestTime = performance.now() - requestStart;
results.push({
success: false,
time: requestTime,
error: error.message
});
errors++;
} finally {
activeRequests--;
}
}
resolve();
}, delay);
});
promises.push(promise);
}
await Promise.all(promises);
const totalTime = Date.now() - startTime;
const successfulRequests = results.filter(r => r.success);
const averageResponseTime = successfulRequests.reduce((sum, r) => sum + r.time, 0) / successfulRequests.length;
const throughput = (completedRequests / totalTime) * 1000; // requests per second
const result: LoadTestResult = {
totalRequests: completedRequests,
successfulRequests: successfulRequests.length,
failedRequests: errors,
averageResponseTime,
throughput,
duration: totalTime,
concurrency,
errorRate: (errors / completedRequests) * 100
};
console.log('負荷テスト結果:');
console.log(` 総リクエスト数: ${result.totalRequests}`);
console.log(` 成功: ${result.successfulRequests}`);
console.log(` 失敗: ${result.failedRequests}`);
console.log(` エラー率: ${result.errorRate.toFixed(2)}%`);
console.log(` 平均応答時間: ${result.averageResponseTime.toFixed(2)}ms`);
console.log(` スループット: ${result.throughput.toFixed(2)} req/sec`);
return result;
}
}
interface BenchmarkResult {
name: string;
averageTime: number;
totalTime: number;
iterations: number;
opsPerSecond: number;
}
interface LoadTestOptions {
concurrency?: number;
duration?: number;
rampUp?: number;
}
interface LoadTestResult {
totalRequests: number;
successfulRequests: number;
failedRequests: number;
averageResponseTime: number;
throughput: number;
duration: number;
concurrency: number;
errorRate: number;
}E2Eテスト
typescript
// E2Eテストヘルパー
class E2ETestHelper {
private browser: any;
private page: any;
async setup(options: E2ETestOptions = {}): Promise<void> {
const { headless = true, viewport = { width: 1280, height: 720 } } = options;
// ブラウザを起動(Puppeteer使用例)
this.browser = await puppeteer.launch({
headless,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
this.page = await this.browser.newPage();
await this.page.setViewport(viewport);
// コンソールログをキャプチャ
this.page.on('console', (msg: any) => {
console.log(`ブラウザコンソール: ${msg.text()}`);
});
// エラーをキャプチャ
this.page.on('pageerror', (error: Error) => {
console.error('ページエラー:', error.message);
});
}
async teardown(): Promise<void> {
if (this.page) {
await this.page.close();
}
if (this.browser) {
await this.browser.close();
}
}
async navigateTo(url: string): Promise<void> {
await this.page.goto(url, { waitUntil: 'networkidle0' });
}
async clickElement(selector: string): Promise<void> {
await this.page.waitForSelector(selector);
await this.page.click(selector);
}
async fillInput(selector: string, value: string): Promise<void> {
await this.page.waitForSelector(selector);
await this.page.type(selector, value);
}
async getText(selector: string): Promise<string> {
await this.page.waitForSelector(selector);
return await this.page.$eval(selector, (el: any) => el.textContent);
}
async waitForElement(selector: string, timeout: number = 5000): Promise<void> {
await this.page.waitForSelector(selector, { timeout });
}
async takeScreenshot(name: string): Promise<void> {
await this.page.screenshot({
path: `screenshots/${name}.png`,
fullPage: true
});
}
async assertElementExists(selector: string): Promise<void> {
const element = await this.page.$(selector);
if (!element) {
throw new Error(`要素が見つかりません: ${selector}`);
}
}
async assertElementText(selector: string, expectedText: string): Promise<void> {
const actualText = await this.getText(selector);
if (actualText !== expectedText) {
throw new Error(`テキストが一致しません。期待値: "${expectedText}", 実際の値: "${actualText}"`);
}
}
async assertPageTitle(expectedTitle: string): Promise<void> {
const actualTitle = await this.page.title();
if (actualTitle !== expectedTitle) {
throw new Error(`ページタイトルが一致しません。期待値: "${expectedTitle}", 実際の値: "${actualTitle}"`);
}
}
async assertUrl(expectedUrl: string): Promise<void> {
const actualUrl = this.page.url();
if (actualUrl !== expectedUrl) {
throw new Error(`URLが一致しません。期待値: "${expectedUrl}", 実際の値: "${actualUrl}"`);
}
}
}
interface E2ETestOptions {
headless?: boolean;
viewport?: { width: number; height: number };
}APIリファレンス
コアインターフェース
typescript
interface TestController {
readonly id: string;
readonly label: string;
readonly items: TestItemCollection;
createTestItem(id: string, label: string, uri?: Uri): TestItem;
createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun;
createRunProfile(
label: string,
kind: TestRunProfileKind,
runHandler: (request: TestRunRequest, token: CancellationToken) => void | Thenable<void>,
isDefault?: boolean
): TestRunProfile;
refreshHandler?: () => void | Thenable<void>;
resolveHandler?: (item: TestItem | undefined) => void | Thenable<void>;
dispose(): void;
}
interface TestItem {
readonly id: string;
label: string;
uri?: Uri;
children: TestItemCollection;
parent?: TestItem;
tags: readonly TestTag[];
canResolveChildren: boolean;
busy: boolean;
range?: Range;
error?: string | MarkdownString;
}
interface TestRun {
readonly name?: string;
readonly token: CancellationToken;
enqueued(test: TestItem): void;
started(test: TestItem): void;
skipped(test: TestItem): void;
failed(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void;
errored(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void;
passed(test: TestItem, duration?: number): void;
appendOutput(output: string, location?: Location, test?: TestItem): void;
addCoverage(fileCoverage: FileCoverage): void;
end(): void;
}
interface TestRunRequest {
readonly include?: readonly TestItem[];
readonly exclude?: readonly TestItem[];
readonly profile?: TestRunProfile;
}
interface FileCoverage {
readonly uri: Uri;
readonly statementCoverage: readonly StatementCoverage[];
readonly branchCoverage: readonly BranchCoverage[];
readonly functionCoverage: readonly FunctionCoverage[];
static fromDetails(
uri: Uri,
statementCoverage: readonly StatementCoverage[],
branchCoverage?: readonly BranchCoverage[],
functionCoverage?: readonly FunctionCoverage[]
): FileCoverage;
}ベストプラクティス
- テスト発見: ファイル監視機能を使用した効率的なテスト発見を実装する
- パフォーマンス: 大規模なテストスイートに対して遅延読み込みとキャッシュを使用する
- ユーザーエクスペリエンス: 明確な進行状況インジケーターとエラーメッセージを提供する
- 統合: 複数のテストフレームワークとランナーをサポートする
- カバレッジ: 包括的なカバレッジ分析を実装する
- レポート: 詳細で実用的なテストレポートを生成する
- デバッグ: シームレスなテストデバッグ機能を提供する
- CI/CD: 継続的インテグレーションシステムとの統合を行う
関連API
- デバッグAPI - テストデバッグ用
- コマンドAPI - テストコマンド用
- ワークスペースAPI - ファイル操作用
- UI API - テストUIコンポーネント用