10日で覚えるCypressDay 9: デバッグとテスト戦略
books.chapter 910日で覚えるCypress

Day 9: デバッグとテスト戦略

今日学ぶこと

  • cy.debug() と cy.pause() の使い方
  • DevToolsコンソールでのデバッグ
  • タイムトラベルデバッグ(Test Runner)
  • スクリーンショットとビデオ録画
  • テストのリトライ設定
  • テストの独立性の原則
  • beforeEach / afterEach の適切な使い方
  • Page Object パターン
  • テストの命名規則とファイル構成

デバッグの基本

テストが失敗したとき、原因を素早く特定することが重要です。Cypressにはデバッグを支援する強力なツールが組み込まれています。

flowchart TB
    subgraph Tools["Cypressのデバッグツール"]
        A["cy.debug()"]
        B["cy.pause()"]
        C["タイムトラベル"]
        D["スクリーンショット"]
        E["ビデオ録画"]
    end

    subgraph Flow["デバッグフロー"]
        F["テスト失敗"] --> G["エラーメッセージ確認"]
        G --> H["タイムトラベルで状態確認"]
        H --> I["cy.pause()で一時停止"]
        I --> J["DevToolsで詳細調査"]
    end

    style Tools fill:#3b82f6,color:#fff
    style Flow fill:#8b5cf6,color:#fff

cy.debug() と cy.pause()

cy.debug()

cy.debug() はテスト実行中にブラウザのDevToolsデバッガーを起動します。直前のコマンドの結果を subject として確認できます。

cy.get('.user-name')
  .debug()  // DevToolsのデバッガーで停止
  .should('contain', 'Taro');

DevToolsのコンソールで subject と入力すると、cy.get() で取得した要素を確認できます。

cy.pause()

cy.pause() はテスト実行を一時停止し、手動でステップ実行できるようにします。

cy.visit('/login');
cy.pause();  // ここで一時停止

cy.get('#username').type('testuser');
cy.pause();  // 入力後の状態を確認

cy.get('#password').type('password123');
cy.get('#login-btn').click();

Test Runnerの「Resume」ボタンまたは「Next」ボタンで実行を再開できます。

使い分け

メソッド 用途 停止場所
cy.debug() 要素やデータの詳細確認 DevToolsデバッガー
cy.pause() テストのステップ実行 Test Runner UI

DevToolsコンソールでのデバッグ

Cypressの Test Runner はChromium系ブラウザ上で動作するため、DevToolsをフルに活用できます。

コンソールログの活用

cy.get('.item-list')
  .then(($el) => {
    // コンソールにjQuery要素を出力
    console.log('Element:', $el);
    console.log('Text:', $el.text());
    console.log('Length:', $el.length);
  });

cy.log() によるテストログ

cy.log('--- ログインテスト開始 ---');
cy.get('#username').type('testuser');
cy.log('ユーザー名を入力しました');

cy.get('#password').type('password123');
cy.log('パスワードを入力しました');

cy.get('#login-btn').click();
cy.log('--- ログインボタンをクリック ---');

cy.log() の出力はTest Runnerのコマンドログに表示されるため、テストの流れを視覚的に追跡できます。

Cypressオブジェクトへのアクセス

DevToolsコンソールから直接Cypressオブジェクトにアクセスできます。

// DevToolsコンソールで実行
Cypress.env()              // 環境変数の確認
Cypress.config()           // 設定の確認
Cypress.spec               // 現在のspecファイル情報

タイムトラベルデバッグ

Cypressの最も強力なデバッグ機能の1つがタイムトラベルです。

flowchart LR
    subgraph Timeline["コマンドログ(タイムライン)"]
        C1["visit('/')"] --> C2["get('.btn')"] --> C3["click()"] --> C4["url()"] --> C5["should('include')"]
    end

    C3 -->|"クリックで<br/>スナップショット表示"| S["DOM スナップショット"]

    style Timeline fill:#22c55e,color:#fff
    style S fill:#f59e0b,color:#000

使い方

  1. テストを実行する
  2. Test Runner左側のコマンドログで任意のコマンドをクリック
  3. そのコマンド実行時のDOMの状態がプレビューに表示される
  4. Before/After を切り替えて、コマンド前後の変化を確認

ピン留め機能

コマンドをクリックすると「ピン留め」され、その時点のDOMが固定表示されます。DevToolsの Elements パネルでその時点のDOM構造を詳しく調査できます。

// 各コマンドの実行時点のDOMを確認可能
cy.visit('/dashboard');           // Step 1: ページ遷移
cy.get('.sidebar').click();       // Step 2: サイドバークリック
cy.get('.menu-item').first().click(); // Step 3: メニュー選択
cy.get('.content').should('be.visible'); // Step 4: コンテンツ表示確認

スクリーンショットとビデオ録画

スクリーンショット

// 任意のタイミングでスクリーンショットを撮影
cy.screenshot('login-page');

// 要素のみをキャプチャ
cy.get('.error-message').screenshot('error-state');

// テスト失敗時は自動でスクリーンショットが保存される

スクリーンショットのデフォルト保存先は cypress/screenshots/ です。

設定のカスタマイズ

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    screenshotsFolder: 'cypress/screenshots',
    screenshotOnRunFailure: true,  // 失敗時の自動撮影
  },
});

ビデオ録画

cypress run(ヘッドレスモード)で実行すると、テストのビデオが自動で録画されます。

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    video: true,                    // ビデオ録画を有効化
    videosFolder: 'cypress/videos', // 保存先
    videoCompression: 32,           // 圧縮レベル(0-51)
  },
});
# ヘッドレスモードで実行(ビデオが録画される)
npx cypress run

スクリーンショット vs ビデオ

機能 スクリーンショット ビデオ
用途 特定時点の状態確認 テスト全体の流れ確認
自動保存 失敗時に自動 run実行時に自動
ファイルサイズ 小さい 大きい
CI/CDでの活用 失敗原因の特定 フロー全体のレビュー

テストのリトライ設定

ネットワーク遅延やアニメーションなどにより、テストが不安定になることがあります。リトライ機能で安定性を向上させましょう。

flowchart TB
    subgraph Retry["リトライの流れ"]
        T["テスト実行"] --> R{"成功?"}
        R -->|"Yes"| P["Pass"]
        R -->|"No"| C{"リトライ<br/>回数超過?"}
        C -->|"No"| T
        C -->|"Yes"| F["Fail"]
    end

    style P fill:#22c55e,color:#fff
    style F fill:#ef4444,color:#fff
    style Retry fill:#8b5cf6,color:#fff

グローバル設定

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  retries: {
    runMode: 2,    // cypress run 時のリトライ回数
    openMode: 0,   // cypress open 時のリトライ回数
  },
});

テスト単位での設定

// 特定のdescribeブロックにリトライを設定
describe('不安定なAPI連携テスト', { retries: 3 }, () => {
  it('データを取得して表示する', () => {
    cy.visit('/dashboard');
    cy.get('.data-table').should('be.visible');
  });
});

// 特定のitブロックにリトライを設定
it('通知が表示される', { retries: { runMode: 3, openMode: 1 } }, () => {
  cy.get('.notification').should('be.visible');
});

リトライの注意点

// NG: リトライしても状態がリセットされない
it('カウンターテスト', () => {
  cy.get('#increment').click();  // リトライ時に再度クリックされる
  cy.get('#count').should('have.text', '1');
});

// OK: beforeEach で状態をリセット
beforeEach(() => {
  cy.visit('/counter');  // 毎回ページをリロード
});

it('カウンターテスト', () => {
  cy.get('#increment').click();
  cy.get('#count').should('have.text', '1');
});

テストの独立性

各テストは他のテストに依存せず、単独で実行できるべきです。

flowchart TB
    subgraph Bad["悪い例: テスト間の依存"]
        B1["テスト1: ユーザー作成"] --> B2["テスト2: ユーザーでログイン"] --> B3["テスト3: プロフィール編集"]
    end

    subgraph Good["良い例: 独立したテスト"]
        G1["テスト1: ユーザー作成"]
        G2["テスト2: ログイン<br/>(APIでユーザー作成)"]
        G3["テスト3: プロフィール編集<br/>(APIでログイン済み状態を作成)"]
    end

    style Bad fill:#ef4444,color:#fff
    style Good fill:#22c55e,color:#fff

悪い例

// NG: テスト間に依存関係がある
describe('ユーザー管理', () => {
  it('ユーザーを作成する', () => {
    cy.visit('/register');
    cy.get('#name').type('Taro');
    cy.get('#email').type('taro@example.com');
    cy.get('#submit').click();
  });

  // このテストはテスト1が成功しないと動かない!
  it('作成したユーザーでログインする', () => {
    cy.visit('/login');
    cy.get('#email').type('taro@example.com');
    cy.get('#password').type('password');
    cy.get('#login-btn').click();
  });
});

良い例

// OK: 各テストが独立している
describe('ユーザー管理', () => {
  it('ユーザーを作成する', () => {
    cy.visit('/register');
    cy.get('#name').type('Taro');
    cy.get('#email').type('taro@example.com');
    cy.get('#submit').click();
    cy.url().should('include', '/dashboard');
  });

  it('ログインする', () => {
    // APIで直接ユーザーを作成(UIに依存しない)
    cy.request('POST', '/api/users', {
      name: 'Taro',
      email: 'taro@example.com',
      password: 'password',
    });

    cy.visit('/login');
    cy.get('#email').type('taro@example.com');
    cy.get('#password').type('password');
    cy.get('#login-btn').click();
    cy.url().should('include', '/dashboard');
  });
});

beforeEach / afterEach の適切な使い方

beforeEach

各テストの前に実行される共通のセットアップ処理です。

describe('ダッシュボード', () => {
  beforeEach(() => {
    // 各テスト前にログイン状態を作成
    cy.request('POST', '/api/login', {
      email: 'test@example.com',
      password: 'password',
    }).then((response) => {
      window.localStorage.setItem('token', response.body.token);
    });

    cy.visit('/dashboard');
  });

  it('ウェルカムメッセージが表示される', () => {
    cy.get('.welcome').should('contain', 'ようこそ');
  });

  it('サイドバーが表示される', () => {
    cy.get('.sidebar').should('be.visible');
  });

  it('統計情報が表示される', () => {
    cy.get('.stats').should('be.visible');
  });
});

afterEach

各テストの後に実行されるクリーンアップ処理です。

describe('データ操作', () => {
  afterEach(() => {
    // テスト後にデータをクリーンアップ
    cy.request('DELETE', '/api/test-data/cleanup');
  });

  it('アイテムを追加する', () => {
    cy.get('#add-btn').click();
    cy.get('.item-list').should('have.length', 1);
  });
});

before / after との違い

フック 実行タイミング 用途
before describe内で1回だけ(最初) DB初期化など重い処理
beforeEach 各itの前に毎回 ページ遷移、ログイン
afterEach 各itの後に毎回 データクリーンアップ
after describe内で1回だけ(最後) 最終クリーンアップ

Page Object パターン

Page Objectパターンは、ページごとの操作をクラスやオブジェクトにまとめるデザインパターンです。テストの可読性と保守性を大幅に向上させます。

flowchart TB
    subgraph Without["Page Objectなし"]
        T1["テストA: cy.get('#email')..."]
        T2["テストB: cy.get('#email')..."]
        T3["テストC: cy.get('#email')..."]
    end

    subgraph With["Page Objectあり"]
        PO["LoginPage<br/>- email入力<br/>- パスワード入力<br/>- ログイン実行"]
        TA["テストA: loginPage.login()"]
        TB["テストB: loginPage.login()"]
        TC["テストC: loginPage.login()"]
        PO --> TA
        PO --> TB
        PO --> TC
    end

    style Without fill:#ef4444,color:#fff
    style With fill:#22c55e,color:#fff
    style PO fill:#3b82f6,color:#fff

Page Objectの作成

// cypress/pages/LoginPage.js
class LoginPage {
  // Selectors
  get emailInput() {
    return cy.get('#email');
  }

  get passwordInput() {
    return cy.get('#password');
  }

  get loginButton() {
    return cy.get('#login-btn');
  }

  get errorMessage() {
    return cy.get('.error-message');
  }

  // Actions
  visit() {
    cy.visit('/login');
    return this;
  }

  typeEmail(email) {
    this.emailInput.clear().type(email);
    return this;
  }

  typePassword(password) {
    this.passwordInput.clear().type(password);
    return this;
  }

  submit() {
    this.loginButton.click();
    return this;
  }

  login(email, password) {
    this.typeEmail(email);
    this.typePassword(password);
    this.submit();
    return this;
  }
}

export default new LoginPage();

テストでの使用

// cypress/e2e/login.cy.js
import loginPage from '../pages/LoginPage';

describe('ログインページ', () => {
  beforeEach(() => {
    loginPage.visit();
  });

  it('正しい認証情報でログインできる', () => {
    loginPage.login('test@example.com', 'password123');
    cy.url().should('include', '/dashboard');
  });

  it('間違ったパスワードでエラーが表示される', () => {
    loginPage.login('test@example.com', 'wrong');
    loginPage.errorMessage.should('contain', 'パスワードが正しくありません');
  });

  it('メールアドレス未入力でエラーが表示される', () => {
    loginPage.typePassword('password123').submit();
    loginPage.errorMessage.should('contain', 'メールアドレスを入力してください');
  });
});

Page Objectのメリット

メリット 説明
保守性 UIが変わってもPage Objectだけ修正すればよい
可読性 テストが意図を明確に表現する
再利用性 同じ操作を複数テストで共有できる
DRY原則 セレクタの重複を排除できる

テストの命名規則とファイル構成

推奨ディレクトリ構成

cypress/
├── e2e/                    # テストファイル
│   ├── auth/
│   │   ├── login.cy.js
│   │   ├── logout.cy.js
│   │   └── register.cy.js
│   ├── dashboard/
│   │   ├── overview.cy.js
│   │   └── settings.cy.js
│   └── products/
│       ├── list.cy.js
│       ├── detail.cy.js
│       └── cart.cy.js
├── fixtures/               # テストデータ
│   ├── users.json
│   └── products.json
├── pages/                  # Page Objects
│   ├── LoginPage.js
│   ├── DashboardPage.js
│   └── ProductPage.js
├── support/                # ヘルパー・カスタムコマンド
│   ├── commands.js
│   └── e2e.js
└── downloads/              # ダウンロードファイル

命名規則

// describe: 機能やページ単位
describe('ログインページ', () => {

  // context: 条件やシナリオ
  context('有効な認証情報の場合', () => {

    // it: 期待する振る舞い(〜すべき / 〜する)
    it('ダッシュボードにリダイレクトする', () => {
      // ...
    });

    it('ウェルカムメッセージを表示する', () => {
      // ...
    });
  });

  context('無効な認証情報の場合', () => {
    it('エラーメッセージを表示する', () => {
      // ...
    });

    it('ログインページに留まる', () => {
      // ...
    });
  });
});

ファイル命名のルール

パターン 用途
機能名.cy.js login.cy.js 単一機能のテスト
機能名-操作.cy.js product-search.cy.js 特定操作のテスト
ページ名.cy.js dashboard.cy.js ページ単位のテスト

まとめ

概念 説明
cy.debug() DevToolsデバッガーで停止して調査
cy.pause() テスト実行を一時停止してステップ実行
タイムトラベル コマンドログから過去のDOM状態を確認
スクリーンショット 特定時点の画面を画像として保存
ビデオ録画 テスト全体の動画を自動録画
リトライ 不安定なテストを自動で再実行
テストの独立性 各テストが他に依存しない設計
Page Object ページ操作をオブジェクトにまとめるパターン
命名規則 describe/context/it の階層構造

重要ポイント

  1. タイムトラベルはCypress最大の強みの1つ
  2. cy.pause()cy.debug() を使い分ける
  3. テストの独立性を常に意識する
  4. beforeEachで共通セットアップを行う
  5. Page Objectパターンで保守性を高める

練習問題

基本

  1. テストの途中に cy.pause() を挿入し、Test Runnerでステップ実行を試してください。
  2. cy.screenshot('my-screenshot') を使って、任意のタイミングでスクリーンショットを撮影してください。
  3. cypress.config.js でリトライ回数を runMode: 2 に設定してください。

応用

  1. ログインページの Page Object を作成し、テストから呼び出してください。
  2. beforeEach を使って、テストごとに初期状態をセットアップする構成に書き換えてください。
  3. describe / context / it を使った階層的な命名規則でテストを整理してください。

チャレンジ

  1. 複数ページにまたがるE2Eテスト(登録 → ログイン → プロフィール編集)を、各テストが独立して動作するように設計してください。API呼び出しを使ってテストの前提条件を作成してください。

参考リンク


次回予告: Day 10では「CI/CDとベストプラクティス」について学びます。GitHub Actionsでの自動テスト実行、並列テスト、パフォーマンス最適化など、実践的なテスト運用を身につけましょう!