testing-framework.md 9.2 KB

テスト戦略 - roo-code知見とPhase 5対応

🧪 テスト設計方針

roo-codeから学んだベストプラクティス

1. 包括的テストカバレッジ

  • 実用的なコード例: 関数、クラス、インデント、タブ/スペース混在
  • エラーケース徹底検証: マーカー検証、シーケンス検証、境界値テスト
  • パフォーマンステスト: 大きなファイル、複数diff処理

2. 段階的バリデーション

// roo-codeテストパターンの適用
describe('GROWI Editor Assistant Validation', () => {
  describe('Phase 1: 基本的なバリデーション', () => {
    it('should require startLine field')
    it('should require non-empty search field')
    it('should accept valid replace field (empty allowed)')
  })
  
  describe('Phase 2: 検索処理', () => {
    it('should find exact matches at specified line')
    it('should find fuzzy matches within threshold')
    it('should reject matches below threshold')
  })
  
  describe('Phase 3: 置換処理', () => {
    it('should replace content at correct position')
    it('should preserve indentation')
    it('should handle multiple replacements in order')
  })
})

📋 Phase 5テスト項目

1. 核心機能テスト

Search-Replace基本動作

describe('Search-Replace Core Functionality', () => {
  test('完全一致での置換', async () => {
    const originalContent = `function test() {
  console.log("hello");
}`;
    const diff = {
      search: 'console.log("hello");',
      replace: 'console.log("world");',
      startLine: 2
    };
    
    const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
    expect(result).toBe(true);
    expect(yText.toString()).toContain('console.log("world");');
  });

  test('Fuzzy Matching(80%類似度)', async () => {
    const originalContent = `function  test() {
    console.log( "hello" );
}`;
    const diff = {
      search: 'console.log("hello");',  // スペース違い
      replace: 'console.log("world");',
      startLine: 2
    };
    
    const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
    expect(result).toBe(true);  // 80%以上の類似度で成功
  });

  test('類似度不足での失敗', async () => {
    const diff = {
      search: 'completely_different_content',
      replace: 'new_content',
      startLine: 2
    };
    
    const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
    expect(result).toBe(false);  // 類似度不足で失敗
  });
});

startLine必須バリデーション

describe('startLine Validation', () => {
  test('サーバー側バリデーション', () => {
    const invalidDiff = { search: 'test', replace: 'new' }; // startLineなし
    expect(() => validateDiffStructure(invalidDiff)).toThrow('startLine is required');
  });

  test('クライアント側バリデーション', () => {
    const diffs = [{ search: 'test', replace: 'new' }]; // startLineなし
    expect(() => validateDiffs(diffs)).toThrow('startLine is required for client processing');
  });
});

2. エラーハンドリングテスト

詳細エラー報告

describe('Error Handling and Reporting', () => {
  test('類似度不足エラーの詳細情報', async () => {
    const result = await fuzzyMatcher.findBestMatch(content, 'nonexistent', { preferredStartLine: 1 });
    
    expect(result.success).toBe(false);
    expect(result.error).toEqual({
      type: 'SIMILARITY_TOO_LOW',
      message: expect.stringContaining('類似度が不十分'),
      details: {
        searchContent: 'nonexistent',
        bestMatch: expect.any(String),
        similarity: expect.any(Number),
        suggestions: expect.arrayContaining([
          'read_fileツールで最新のファイル内容を確認',
          '空白やインデントの違いを確認'
        ])
      }
    });
  });

  test('修正提案の生成', () => {
    const error = createSimilarityError('search_text', 'best_match', 0.6);
    expect(error.details.suggestions).toContain('類似度の閾値を下げることを検討');
  });
});

3. インデント・フォーマット保持テスト

roo-code互換のインデント処理

describe('Indentation and Formatting', () => {
  test('タブインデントの保持', async () => {
    const originalContent = `function test() {
\tconsole.log("hello");
}`;
    const diff = {
      search: '\tconsole.log("hello");',
      replace: '\tconsole.log("world");',
      startLine: 2
    };
    
    const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
    expect(yText.toString()).toContain('\tconsole.log("world");');
  });

  test('スペースインデントの保持', async () => {
    const originalContent = `function test() {
    console.log("hello");
}`;
    const diff = {
      search: '    console.log("hello");',
      replace: '    console.log("world");',
      startLine: 2
    };
    
    const result = await performSearchReplace(yText, diff.search, diff.replace, diff.startLine);
    expect(yText.toString()).toContain('    console.log("world");');
  });

  test('混在インデントの処理', async () => {
    // タブとスペースが混在する場合の正規化テスト
  });
});

4. 複数diff処理テスト

順序・競合処理

describe('Multiple Diff Processing', () => {
  test('複数diffの順序処理', async () => {
    const diffs = [
      { search: 'line1', replace: 'newLine1', startLine: 1 },
      { search: 'line3', replace: 'newLine3', startLine: 3 },
      { search: 'line2', replace: 'newLine2', startLine: 2 }
    ];
    
    // startLineでソートされて処理されることを確認
    const result = await applyMultipleDiffs(yText, diffs);
    expect(result.appliedCount).toBe(3);
    expect(result.success).toBe(true);
  });

  test('部分失敗時の処理継続', async () => {
    const diffs = [
      { search: 'existing', replace: 'new1', startLine: 1 },
      { search: 'nonexistent', replace: 'new2', startLine: 2 },
      { search: 'existing2', replace: 'new3', startLine: 3 }
    ];
    
    const result = await applyMultipleDiffs(yText, diffs);
    expect(result.appliedCount).toBe(2);  // 1つ失敗、2つ成功
    expect(result.failedParts).toHaveLength(1);
  });
});

5. パフォーマンステスト

roo-code準拠の効率検証

describe('Performance Tests', () => {
  test('大きなファイルでの処理時間', async () => {
    const largeContent = 'line\n'.repeat(10000);  // 10,000行
    const startTime = performance.now();
    
    const result = await performSearchReplace(
      createYTextFromString(largeContent),
      'line',
      'newLine',
      5000  // 中央付近の行
    );
    
    const endTime = performance.now();
    expect(endTime - startTime).toBeLessThan(1000);  // 1秒以内
    expect(result).toBe(true);
  });

  test('Middle-out検索の効率性', async () => {
    // 指定行から近い位置にある内容が素早く見つかることを確認
    const content = generateTestContent(1000);
    const matcher = new ClientFuzzyMatcher();
    
    const result = await matcher.findBestMatch(content, 'target_line', {
      preferredStartLine: 500,
      bufferLines: 20
    });
    
    expect(result.success).toBe(true);
    expect(result.matchedLine).toBeCloseTo(500, 20);  // 指定行から20行以内
  });
});

🎯 Phase 5テスト実行指針

優先度1: 必須動作確認

  1. startLine必須バリデーション: サーバー・クライアント両方
  2. 基本的なsearch-replace: 完全一致での置換
  3. Fuzzy Matching: 80%閾値での動作

優先度2: エラーハンドリング

  1. 詳細エラー報告: 失敗理由と修正提案
  2. 部分失敗処理: 一部差分の失敗時の継続処理
  3. バリデーションエラー: 不正データの適切な処理

優先度3: 高度機能

  1. インデント保持: タブ・スペース・混在の処理
  2. 複数diff順序: startLineによる適切なソート
  3. パフォーマンス: 大きなファイルでの応答性

🔧 テスト環境セットアップ

テストデータ生成

// テスト用のコンテンツ生成
export function createTestContent(type: 'javascript' | 'typescript' | 'mixed') {
  switch (type) {
    case 'javascript':
      return `function test() {
  console.log("hello");
  return true;
}`;
    case 'typescript':
      return `interface User {
  name: string;
  age: number;
}`;
    case 'mixed':
      return `function test() {
\tconsole.log("tab indent");
    console.log("space indent");
}`;
  }
}

モックYText実装

// Yjs YTextのモック実装
export class MockYText {
  private content: string = '';
  
  toString(): string { return this.content; }
  insert(index: number, text: string): void { /* 実装 */ }
  delete(index: number, length: number): void { /* 実装 */ }
}

テスト戦略作成日: 2025-06-18
参考資料: roo-code test suite (1,186行)
対象Phase: Phase 2A・2B実装の検証