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
使い方
- テストを実行する
- Test Runner左側のコマンドログで任意のコマンドをクリック
- そのコマンド実行時のDOMの状態がプレビューに表示される
- 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 の階層構造 |
重要ポイント
- タイムトラベルはCypress最大の強みの1つ
- cy.pause() と cy.debug() を使い分ける
- テストの独立性を常に意識する
- beforeEachで共通セットアップを行う
- Page Objectパターンで保守性を高める
練習問題
基本
- テストの途中に
cy.pause()を挿入し、Test Runnerでステップ実行を試してください。 cy.screenshot('my-screenshot')を使って、任意のタイミングでスクリーンショットを撮影してください。cypress.config.jsでリトライ回数をrunMode: 2に設定してください。
応用
- ログインページの Page Object を作成し、テストから呼び出してください。
beforeEachを使って、テストごとに初期状態をセットアップする構成に書き換えてください。describe/context/itを使った階層的な命名規則でテストを整理してください。
チャレンジ
- 複数ページにまたがるE2Eテスト(登録 → ログイン → プロフィール編集)を、各テストが独立して動作するように設計してください。API呼び出しを使ってテストの前提条件を作成してください。
参考リンク
次回予告: Day 10では「CI/CDとベストプラクティス」について学びます。GitHub Actionsでの自動テスト実行、並列テスト、パフォーマンス最適化など、実践的なテスト運用を身につけましょう!