kickflow Tech Blog

株式会社kickflowのプロダクト開発本部によるブログ

AIとペアプロで100件超の監査ログテストをCypressで実装しました

はじめに

こんにちは。kickflow QAエンジニアのNです。
今回は、AIの力を借りながらCypressで稟議ツールの重要な機能の1つである「監査ログ」のE2Eテストを実装した取り組みについてお話しします。


概要

監査ログとは?

監査ログとは、監査対応のために管理者が管理画面内で実行された操作の履歴を記録する機能です。
「誰が」「いつ」「何を」実行したかを記録することで、セキュリティ監査やトラブルシューティングに活用できます。

今回の取り組み

kickflowの監査ログ機能は、100種類以上のアクション(組織管理、ワークフロー、外部連携など)を記録します。
これらすべての監査ログが正しく記録されることを確認するE2Eテストを、AIとペアプログラミングしながら実装しました。

最終的な成果

  • 実装完了項目: 159項目(全171項目中)
  • カバレッジ: 93.0%
  • テストファイル数: 40以上
  • 対応環境: 15以上の専用環境
  • 実装期間: 約2週間

AIとのペアプログラミング

Roo Code × Claude

今回はRoo Code(VSCodeの拡張機能)を使ったClaudeとのペアプログラミングを利用しました。

使用したツール

  • Roo Code: VSCode拡張機能
    • エディタ内でClaudeと直接対話
    • ファイルの読み書きをシームレスに実行
    • コンテキストを保持しながら連続作業が可能
  • Claude (Anthropic): AI アシスタント
    • コード生成・リファクタリング
    • パターン認識と提案
    • ドキュメント作成支援

AIの役割

  1. パターン認識: 類似コードのパターンを素早く認識
  2. コード生成: 繰り返しパターンの自動生成
  3. リファクタリング: より良い設計の提案
  4. ドキュメント作成: 実装状況表やガイドの作成

人間(私)の役割

  1. 要件定義: テスト方針の決定
  2. 品質確認: テスト実行による動作確認
  3. 細かい修正: 名称や表現の調整
  4. PR作成とレビュー: 実装内容のレビューとマージ

AIを効果的に使うための工夫

大規模な実装だったため、AIを活用する際、いくつかの工夫をしました。

1. タスクの細分化とリセット

1つのタスクで長時間作業を続けると、トークン数が超過し突然エラーが発生して使えなくなってしまう事象が発生しました。

実際に起きた失敗例:
- 1つのタスクで複数のテストファイルを実装
- トークン数が増加し続ける
- それまでのコンテキストが失われる
- ファイルが壊れる
- 作業続行不能

解決策として、タスクごとに新しいタスクを開始することにしました。

改善後のワークフロー:
1. 1つのテストファイル実装 → タスク完了
2. 新しいタスクを開始
3. 次のテストファイル実装 → タスク完了
4. (繰り返し)

この方法により、トークン数を常に低く保ちエラーによる作業ロスを防ぎ各タスクを独立して管理しやすいという大きな効果を得られました。

2. 作業指示書の外部管理

200項目近くの実装を進める中で、毎回同じ説明をするのは非効率でした。

そこで、ローカルに以下の内容を記載した作業指示書を作成しました。

  • プロジェクト全体の方針
  • テスト実装のパターン
  • 環境ごとの設定方法
  • よくある問題と解決策
  • 実装済みファイルの一覧

使い方

# 新しいタスクを開始するたび

1. 作業指示書をAIに読み込ませる
2. 「この指示書に従って、〇〇の監査ログテストを実装してください」
3. 実装完了後、タスクを閉じる
4. 次のタスクでも同じ作業指示書を使用

この作業指示書の外部管理により、毎回同じ説明をする手間が省けるだけでなく、トークンリセット後も一貫した品質を保てるようになり、ナレッジとして蓄積できるという大きなメリットが得られました。

3. 実装パターンの標準化

作業指示書に以下のような標準パターンを記載しました。

// テンプレート例
describe('監査ログテスト: [カテゴリ名]', () => {
  before(() => {
    // 必要に応じて環境設定
  })

  beforeEach(() => {
    cy.login('/admin/audit', 'USER_ID')
    setupAuditLogPage({ category: '[カテゴリ名]' })
  })

  it('[操作名]の監査ログが記録される', () => {
    verifyAuditLog('[ログテキスト]')
  })
})

この標準化により、AIは毎回一貫したコードを生成できるようになりました。

AIとの協働で得た知見

  1. トークン管理が重要: 大規模作業では、トークン数を常に意識する
  2. 外部ドキュメントの活用: 作業指示書で知識を外部化・永続化する
  3. タスクの適切な粒度: 1タスク = 1〜3ファイル程度が理想的
  4. パターンの明示: 明確なパターンを示すことでAIの生成品質が向上

実装内容詳細

1:実装方針の転換:データ作成からログ検証へ

最初は「テストデータをAPIで作成→操作を実行→ログ検証」という方針でしたが、Autifyの既存テストが既に操作を実行していることに気づき、「既存ログの出力検証をする」方針に転換しました。

これにより、以下のような効果が得られました。

  • テストコードのシンプル化: 複雑なデータ作成処理が不要に
  • 実装時間の短縮: ログ検証に集中できることで効率的な実装が可能に
  • 実運用に近いテスト: 実際の運用環境に近い状態でのテストを実現

実装方針のBefore/After

// Before: データ作成から始める
beforeEach(() => {
  // テストデータを作成
  cy.createWorkflow()
  cy.updateWorkflow()
  // ...
})

// After: 既存ログの検証に集中
beforeEach(() => {
  cy.login('/admin/audit', 'USER_ID')
  setupAuditLogPage({ category: 'ワークフロー管理' })
})

it('ワークフローを作成の監査ログが記録される', () => {
  verifyAuditLog('ワークフローを作成')
})

2:実装進捗の可視化:進捗表の作成

実装を効率的に進めていくにあたって、100種類以上の監査ログアクションを管理するため、詳細な進捗表を作成しました。

進捗表の構成

カテゴリ 総数 完了 実装中 未実装 対象外 進捗率
組織系 45 45 0 0 0 100%
ワークフロー系 45 44 0 0 1 100%

...

詳細な実装状況管理

各アクションごとに以下を記録:

アクション名 操作内容 ステータス 担当者 備考
organization_user_invite ユーザーを招待 🟢 完了 Claude audit-organization-user.cy.ts実装済み
organization_user_update ユーザーを編集 🟢 完了 Claude audit-organization-user.cy.ts実装済み

この進捗表を作成したことで、実装状況の一目での把握でき、未実装項目の素早い特定し、全体の進捗率の継続的な確認が可能になりました。
また、正確な監査ログの操作内容の名称を一覧で把握できるという副次的なメリットもありました。

3:営業日を考慮した日付フィルター設計

kickflowでは平日は毎朝E2Eの自動テストを実行していますが、単純に「今日 or 昨日のログを確認」という実装では週明けや祝日を挟んだ場合、実行タイミングによって失敗してしまいます。
そこで、実行日から4日以内のログを指定することにしました。

Before:1日を指定

// NG例:月曜日実行 → 日曜日を検索 → テスト実行なし → ログなし → 失敗
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)

After:4日以内のログを確認

/**
 * 日付範囲を生成する関数
 * @param daysBack 何日前からのデータを取得するか(デフォルト: 4日)
 */
export function getDateRange(daysBack: number = 4) {
  const endDate = new Date()
  const startDate = new Date(endDate.getTime() - daysBack * 24 * 60 * 60 * 1000)

  return {
    startDate: formatDate(startDate),
    endDate: formatDate(endDate),
  }
}

この設計により、週末や祝日を挟んだ場合でもテストが成功し、4日以内の操作を確実に検証できるようになりました。

4:ページ表示件数の動的変更

監査ログは、初期表示で1ページ25件しか表示されないため、操作の多い環境では目的のログが表示されないという問題がありました。
そこで、表示件数プルダウンで、25件→100件へ表示変更の操作を追加しました。

実装

export function setupAuditLogPage(options: AuditLogSetupOptions): void {
  const { category, daysBack = 4, itemsPerPage = 100 } = options

  // ...日付フィルター設定

  // 1ページあたりの件数を変更
  if (itemsPerPage !== 25) {
    cy.get('.v-data-table-footer__items-per-page .v-select .v-field').click()
    cy.get('.v-overlay-container').contains(String(itemsPerPage)).click()
    cy.wait(2000)
  }
}

使用例

// 操作が多い環境では100件表示
setupAuditLogPage({
  category: 'ワークフロー管理',
  itemsPerPage: 100,
})

5:リファクタリング:共通関数の作成

実装が進むにつれて、同じパターンのコードが繰り返し出現するようになりました。
そこで、共通関数化を実施しました。

Before:リファクタリング前

各テストファイルで40行以上の重複コード:

describe('監査ログテスト: ワークフロー管理', () => {
  beforeEach(() => {
    cy.login('/admin/audit', 'USER_ID')

    // 操作カテゴリ設定
    cy.contains('.v-label', '操作カテゴリ').parents('.v-input').click()
    cy.get('.v-overlay-container').contains('ワークフロー管理').click()

    // 日付フィルター設定
    const endDate = new Date()
    const startDate = new Date(endDate.getTime() - 4 * 24 * 60 * 60 * 1000)
    const formatDate = (date: Date): string => date.toISOString().split('T')[0]

    cy.contains('.v-label', '作成日').parents('.v-input').find('input').click()
    cy.contains('.v-label', '作成日')
      .parents('.v-input')
      .find('input')
      .type(`${formatDate(startDate)}~${formatDate(endDate)}`)

    // ページ件数変更
    cy.get('.v-data-table-footer__items-per-page .v-select .v-field').click()
    cy.get('.v-overlay-container').contains('100').click()

    cy.wait(2000)
  })

  it('フォルダを作成の監査ログが記録される', () => {
    cy.get('body').then($body => {
      const hasLog = $body.text().includes('フォルダを作成')
      expect(hasLog, 'フォルダを作成のログが存在すること').to.equal(true)
    })
  })

  it('フォルダを編集の監査ログが記録される', () => {
    cy.get('body').then($body => {
      const hasLog = $body.text().includes('フォルダを編集')
      expect(hasLog, 'フォルダを編集のログが存在すること').to.equal(true)
    })
  })
})

共通関数の作成

以下の2つの共通関数を作成しました。

1. setupAuditLogPage() - ページセットアップ

export function setupAuditLogPage(options: AuditLogSetupOptions): void {
  const { category, daysBack = 4, itemsPerPage = 100 } = options

  // 日付範囲を取得
  const endDate = new Date()
  const startDate = new Date(endDate.getTime() - daysBack * 24 * 60 * 60 * 1000)
  const formatDate = (date: Date): string => date.toISOString().split('T')[0]

  // 操作カテゴリを設定
  cy.contains('.v-label', '操作カテゴリ').parents('.v-input').click()
  cy.get('.v-overlay-container').contains(category).click()

  // 作成日フィルターを設定
  cy.contains('.v-label', '作成日').parents('.v-input').find('input').click()
  cy.contains('.v-label', '作成日')
    .parents('.v-input')
    .find('input')
    .type(`${formatDate(startDate)}~${formatDate(endDate)}`)

  cy.wait(2000)

  // 1ページあたりの件数を変更
  if (itemsPerPage !== 25) {
    cy.get('.v-data-table-footer__items-per-page .v-select .v-field').click()
    cy.get('.v-overlay-container').contains(String(itemsPerPage)).click()
    cy.wait(2000)
  }
}

2. verifyAuditLog() - 単一ログ検証

export function verifyAuditLog(logText: string, message?: string): void {
  cy.get('body').then($body => {
    const hasLog = $body.text().includes(logText)
    const assertionMessage = message || `${logText}のログが存在すること`
    expect(hasLog, assertionMessage).to.equal(true)
  })
}

After:リファクタリング後

共通関数を使うことでわずか数行に!

describe('監査ログテスト: ワークフロー管理', () => {
  beforeEach(() => {
    cy.login('/admin/audit', 'USER_ID')
    setupAuditLogPage({ category: 'ワークフロー管理' })
  })

  it('フォルダを作成の監査ログが記録される', () => {
    verifyAuditLog('フォルダを作成')
  })

  it('フォルダを編集の監査ログが記録される', () => {
    verifyAuditLog('フォルダを編集')
  })
})

なぜ単一ログ検証(個別検証)を選んだのか

各ログを個別に検証する設計を採用しましたが、これには理由がありました。

1. 確実性の向上
各ログが確実に1つずつ出力されていることを個別に確認したい。
現在、実行時間に懸念はないため、パフォーマンスよりも確実性を優先しました。

2. 不具合の早期発見

// もし一括検証を使った場合
it('フォルダ操作の監査ログが記録される', () => {
  verifyAuditLogs(['フォルダを作成', 'フォルダを編集', 'フォルダを削除'])
  // → どれかが失敗しても、どのログで失敗したかが分かりにくい
})

// 個別検証の場合
it('フォルダを作成の監査ログが記録される', () => {
  verifyAuditLog('フォルダを作成')
  // → テスト結果で「フォルダを作成」が失敗と一目でわかる
})

テスト結果を確認する際、個別のitケースが失敗していれば、どの操作のログが出力されていないのか一目瞭然です。
これにより、不具合の発生箇所を素早く特定し、迅速な対応が可能になります。

3. CIでの可視性
CIの実行結果画面でも、個別のテストケースとして表示されます。

  • どのログが失敗しているか一目でわかる
  • 過去の実行履歴から特定のログの安定性を追跡できる
  • 再実行時に失敗したケースだけを実行できる

このように、確実性デバッグのしやすさを重視しました。

リファクタリングの効果

このリファクタリングにより、約40行のコードがわずか2行に削減(95%削減)されました。

6:ディレクトリ構造の整理

40以上のテストファイルを管理しやすくするため、カテゴリ別にフォルダを整理しました。

Before

audit-logs/
├── audit-workflow-basic.cy.ts
├── audit-workflow-folder.cy.ts
├── audit-organization-user.cy.ts
├── audit-external-webhook.cy.ts
└── ... (40+ files)

After

audit-logs/
├── core/              # 基本機能・共通ファイル
│   ├── audit-log-helpers.ts
│   ├── date-helper.ts
│   └── audit-log-implementation-status.md
├── organization/      # 組織系(6ファイル)
├── workflow/          # ワークフロー系(9ファイル)
├── external/          # 外部連携系(7ファイル)
├── tenant/            # テナント系(6ファイル)
├── security/          # セキュリティ系(3ファイル)
└── management/        # 管理系(6ファイル)

7:複数環境(複数テストテナント)への対応

監査ログテストは、機能ごとに異なるテスト環境で操作が実施されていたので、1ファイル内で複数環境を使用する必要がありました。

環境設定の例

// 標準環境(環境変数から取得)
describe('監査ログテスト: ワークフロー基本操作', () => {
  // before()なし = 環境変数BASE_URLを使用

  beforeEach(() => {
    cy.login('/admin/audit', 'USER_ID')
    setupAuditLogPage({ category: 'ワークフロー管理' })
  })
})

// 専用環境
describe('監査ログテスト: SSO', () => {
  before(() => {
    Cypress.config('baseUrl', 'https://EXAMPLE_DOMAIN')
  })

  beforeEach(() => {
    cy.login('/admin/audit', 'USER_ID')
    setupAuditLogPage({ category: 'セキュリティ' })
  })
})

Cypress.config()の落とし穴:設定の引き継ぎ問題

複数環境を扱う上で注意が必要だったのが、Cypress.config() で設定した値が後続のすべての describe ブロックに引き継がれるという仕様です。

問題のあるコード例
describe('環境Aのテスト', () => {
  before(() => {
    Cypress.config('baseUrl', 'https://EXAMPLE_DOMAIN_A')
  })
  it('テスト1', () => {
    cy.visit('/page')
  })
})

describe('環境Bのテスト', () => {
  // baseUrlを設定していないが、前の設定が引き継がれる
  it('テスト2', () => {
    cy.visit('/page') // 環境Aにアクセスしてしまう!
  })
})

Cypress.config()はグローバルな設定を変更するため、スペックファイル実行中は設定が保持され続けます。

解決策

すべてのdescribeで明示的に環境を設定することで解決しました!

describe('環境Aのテスト', () => {
  before(() => {
    Cypress.config('baseUrl', 'https://EXAMPLE_DOMAIN_A')
  })
  // ...
})

describe('環境Bのテスト', () => {
  before(() => {
    Cypress.config('baseUrl', 'https://EXAMPLE_DOMAIN_B')
  })
  // ...
})

8:実装対象外としたログについて

しかし、すべての監査ログを実装できたわけではありません。
技術的な制約や実装困難性から、12項目(全171項目中)を対象外としました。

対象外の理由

  1. 外部サービス認証が必要
      ・Slack、Google Workspace、Microsoft Teams、Google Drive連携
      ・外部サービスの認証画面を経由する必要があり、自動化が困難

  2. 機能未実装・仕様変更
      ・Power Automate連携(機能廃止により対象外)
      ・チケットエクスポート(仕様変更により対象外)

  3. セキュリティリスク
      ・IPアドレス制限の編集(ログイン不能リスク)

対象外とした項目については、手動テストやAutifyでの定期テストで品質を担保しています。

9:副次的な効果:既存テストの改善

監査ログの全種(100種類以上)を確認する過程で、予想外の発見がありました。
監査ログを確認していると、一部の操作のログが記録されていないことに気づきました。
これは、Autifyの既存テストでその操作がカバーされていないということ…!

確認例:
監査ログを確認 → 「ワークフローのエクスポート」のログがない
           ↓
既存テストを確認 → エクスポート操作が実行されていない
           ↓
Autifyのテストに追加が必要

この発見により、テストカバレッジの可視化(監査ログの有無で100種類以上の操作を網羅的にチェック)と既存テストの拡張(不足している操作を特定してAutifyに追加)という改善ができました。

監査ログテストの実装が、既存のテスト全体の品質向上にもつながるという、一石二鳥の効果が得られました。

今後の改善課題

現在の実装では、4日以内のログを確認する設計になっています。
これにより、監査ログの出力に不具合があった場合、検知まで最大4日かかるという課題があります。

以下のように、Autifyのテスト実行直後にCypressテストを実行する運用にすることで、今後、テスト実行の運用を見直し、この改善を実施していく予定です。

改善後のワークフロー:
1. Autifyのテスト実行(操作を実行)
 ↓ 完了後すぐに
2. Cypressのテスト実行(ログ検証)
   - 日付レンジの閾値を0日に設定
   - 当日の最新ログのみを確認

まとめ

コードベースのE2E実装経験がほとんどないQAエンジニアでも、AIの力を借りながら大規模なCypressのテストを実装することができました。
今回学んだことは以下です!

1. 実装方針の柔軟性

「データ作成から始める」という固定観念を捨て、「既存ログを検証する」という方針に転換したことで、実装速度が大幅に向上しました。

2. 進捗の可視化

大規模な実装では、進捗の可視化が重要でした。
詳細な進捗表により、常に全体像を把握しながら実装を進めることができましたし、私としても着実に進んでいることがわかって高いモチベーションを保つことができました。

3. リファクタリングの重要性

共通関数を作成するリファクタリングにより、コードの保守性が向上しました。
コードの重複を減らすことができ、可読性がかなり向上しました。

4. AIとの協働

AIは強力なツールですが、要件定義や品質確認は人間が行う必要がありました。
人間が要件定義や実行確認をしている間に、AIにコード実装を進めてもらったことで、効率的に作業を進めることができました。


kickflow では、プロダクトの価値を一緒に高めていく仲間を募集しています。ぜひ採用サイトもご覧ください。

careers.kickflow.co.jp