テストツール
Cypress
概要
Cypressは開発者体験を重視したモダンなE2Eテストフレームワークで、リアルブラウザでの自動テストを実現し、優れたデバッグ機能とリアルタイムリロードを提供します。JavaScript/TypeScriptベースで書きやすく、タイムトラベルデバッグ、自動待機、ネットワークスタビング機能により、信頼性の高いEnd-to-Endテストとインテグレーションテストを実現します。Electron、Chrome、Firefox、Edgeをサポートし、直感的なUIとテスト実行時のリアルタイム確認により、従来のSeleniumベースのテストツールと比較して格段に優れた開発者体験を提供する次世代のWebアプリケーションテストソリューションです。
詳細
Cypress 2025年版は、開発者ファーストのE2Eテストフレームワークとして確固たる地位を築いています。ブラウザ内で直接実行される独自のアーキテクチャにより、従来のWebDriverベースのツールでは不可能だった高速で安定したテスト実行を実現。タイムトラベルデバッグ機能では、テスト実行中の各ステップを巻き戻して確認でき、スナップショット、DOM要素の状態、ネットワークリクエスト、コンソールログを詳細に検証可能です。自動待機(Auto-wait)機能により、明示的な待機コードを書く必要がなく、要素の表示、アニメーション完了、XHRリクエストの完了を自動的に待機します。ネットワークスタビング機能では、APIレスポンスをモックし、エラー状態やエッジケースを容易にテスト可能。Real-time reloadingにより、テストコードの変更が即座に反映され、イテレーション速度が大幅に向上します。Component Testingにも対応し、ReactやVueのコンポーネント単体テストも同じ環境で実行可能です。
主な特徴
- タイムトラベルデバッグ: テスト実行の各ステップを巻き戻し確認可能
- 自動待機機能: 要素やネットワークリクエストの自動待機でフラキーテスト削減
- リアルタイムリロード: テストコード変更の即座反映と高速イテレーション
- ネットワークスタビング: APIレスポンスのモックとエラー状態テスト
- 優れた開発者体験: 直感的なUIと詳細なエラーメッセージ
- クロスブラウザ対応: Chrome、Firefox、Edge、Electronでのテスト実行
メリット・デメリット
メリット
- 直感的なAPIと優れた開発者体験による学習コストの低さ
- タイムトラベルデバッグによる強力なテストデバッグ機能
- 自動待機機能によるフラキーテストの大幅削減と安定したテスト実行
- リアルタイムテスト実行確認とホットリロードによる高速開発サイクル
- ネットワークスタビング機能による包括的なAPI動作テスト
- JavaScript/TypeScript のみでテスト記述が可能で言語習得不要
- 豊富なプラグインエコシステムと活発なコミュニティサポート
- Component TestingとE2E Testing統合による一貫したテスト環境
デメリット
- ブラウザ内実行によるネイティブブラウザイベントの制限
- マルチタブやマルチウィンドウテストの複雑さと制約
- Playwrightと比較してクロスブラウザテストの範囲が限定的
- セキュリティ制限により一部のブラウザ機能アクセスが不可
- 大規模なテストスイートでのメモリ使用量増加
- iOS Safari や古いブラウザバージョンへの対応が限定的
参考ページ
- Cypress公式サイト
- Cypress公式ドキュメント
- Cypress GitHub リポジトリ
- Cypress Examples
- Cypress プラグインリスト
- Cypress ベストプラクティス
書き方の例
インストールとセットアップ
# プロジェクトへのCypressインストール
npm install --save-dev cypress
# Cypressを初期化
npx cypress open
# または、ヘッドレスでテスト実行
npx cypress run
# 特定のブラウザでテスト実行
npx cypress run --browser chrome
npx cypress run --browser firefox
npx cypress run --browser edge
# プロジェクト構造の初期化
# cypress/ フォルダと設定ファイルが自動生成される
# ├── cypress/
# │ ├── e2e/
# │ ├── fixtures/
# │ ├── support/
# │ └── downloads/
# └── cypress.config.js
基本的なE2Eテスト
// cypress/e2e/basic-test.cy.js
describe('基本的なE2Eテスト', () => {
beforeEach(() => {
// テスト開始前の共通処理
cy.visit('https://example.com')
})
it('ページタイトルとコンテンツの確認', () => {
// ページタイトルの確認
cy.title().should('include', 'Example')
// 特定の要素の存在確認
cy.get('h1').should('be.visible')
cy.get('h1').should('contain.text', 'Example Domain')
// 複数の要素の確認
cy.get('p').should('have.length.greaterThan', 0)
cy.contains('More information').should('exist')
})
it('リンクのクリックとナビゲーション', () => {
// リンクをクリック
cy.contains('More information').click()
// URL変更の確認(Cypressは自動的にページロードを待機)
cy.url().should('include', 'iana.org')
// 戻るボタンの動作確認
cy.go('back')
cy.url().should('eq', 'https://example.com/')
})
it('要素の状態確認とアサーション', () => {
// 要素の表示状態確認
cy.get('body').should('be.visible')
// CSS属性の確認
cy.get('h1').should('have.css', 'text-align', 'center')
// 属性値の確認
cy.get('a').first().should('have.attr', 'href')
// テキスト内容の詳細確認
cy.get('p').first().should('contain.text', 'domain')
cy.get('p').first().should('match', /domain.*examples/)
})
})
フォーム操作とインタラクション
// cypress/e2e/form-interaction.cy.js
describe('フォーム操作テスト', () => {
beforeEach(() => {
cy.visit('https://example.com/contact')
})
it('基本的なフォーム入力テスト', () => {
// テキスト入力
cy.get('[data-cy="name"]').type('田中太郎')
cy.get('[data-cy="email"]').type('[email protected]')
// テキストエリアへの入力
cy.get('[data-cy="message"]').type('お問い合わせ内容をここに記載します。')
// セレクトボックスの選択
cy.get('[data-cy="category"]').select('技術的な質問')
// チェックボックスの操作
cy.get('[data-cy="newsletter"]').check()
cy.get('[data-cy="newsletter"]').should('be.checked')
// ラジオボタンの選択
cy.get('[data-cy="priority-high"]').check()
cy.get('[data-cy="priority-high"]').should('be.checked')
})
it('フォーム送信とバリデーション', () => {
// 空のフォーム送信でバリデーションエラー確認
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="error-message"]').should('be.visible')
cy.get('[data-cy="error-message"]').should('contain', '必須項目')
// 正しいデータを入力
cy.get('[data-cy="name"]').type('山田花子')
cy.get('[data-cy="email"]').type('[email protected]')
cy.get('[data-cy="message"]').type('テストメッセージです。')
// フォーム送信
cy.get('[data-cy="submit"]').click()
// 成功メッセージの確認
cy.get('[data-cy="success-message"]').should('be.visible')
cy.get('[data-cy="success-message"]').should('contain', '送信完了')
// URL変更の確認
cy.url().should('include', '/success')
})
it('ファイルアップロードテスト', () => {
// ファイルアップロード(cypress-file-uploadプラグイン使用)
const fileName = 'test-image.jpg'
cy.get('[data-cy="file-upload"]').selectFile({
contents: Cypress.Buffer.from('fake image content'),
fileName: fileName,
mimeType: 'image/jpeg',
})
// アップロードファイル名の確認
cy.get('[data-cy="file-name"]').should('contain', fileName)
// プレビュー要素の表示確認
cy.get('[data-cy="file-preview"]').should('be.visible')
})
})
ネットワークスタビングとAPIテスト
// cypress/e2e/api-stubbing.cy.js
describe('ネットワークスタビングテスト', () => {
beforeEach(() => {
// APIエンドポイントのスタブ設定
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: '太郎', email: '[email protected]' },
{ id: 2, name: '花子', email: '[email protected]' }
]
}).as('getUsers')
})
it('正常なAPIレスポンステスト', () => {
cy.visit('/dashboard')
// API呼び出しの待機
cy.wait('@getUsers')
// レスポンスデータの表示確認
cy.get('[data-cy="user-list"]').should('contain', '太郎')
cy.get('[data-cy="user-list"]').should('contain', '花子')
// ユーザー数の確認
cy.get('[data-cy="user-item"]').should('have.length', 2)
})
it('APIエラー状態のテスト', () => {
// エラーレスポンスのスタブ設定
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Internal Server Error' }
}).as('getUsersError')
cy.visit('/dashboard')
cy.wait('@getUsersError')
// エラーメッセージの表示確認
cy.get('[data-cy="error-message"]').should('be.visible')
cy.get('[data-cy="error-message"]').should('contain', 'サーバーエラー')
})
it('リクエストデータの検証', () => {
// POST リクエストのスタブ
cy.intercept('POST', '/api/users', {
statusCode: 201,
body: { id: 3, name: '新規ユーザー', email: '[email protected]' }
}).as('createUser')
cy.visit('/users/new')
// フォーム入力
cy.get('[data-cy="name"]').type('新規ユーザー')
cy.get('[data-cy="email"]').type('[email protected]')
cy.get('[data-cy="submit"]').click()
// リクエストデータの検証
cy.wait('@createUser').then((interception) => {
expect(interception.request.body).to.deep.equal({
name: '新規ユーザー',
email: '[email protected]'
})
})
})
it('動的レスポンスとシナリオテスト', () => {
// 段階的なレスポンス変更
cy.intercept('GET', '/api/status', { statusCode: 202, body: { status: 'pending' } })
.as('getStatusPending')
cy.visit('/process')
cy.get('[data-cy="start-process"]').click()
cy.wait('@getStatusPending')
// ステータス更新
cy.intercept('GET', '/api/status', { statusCode: 200, body: { status: 'completed' } })
.as('getStatusCompleted')
// ポーリング処理の確認
cy.get('[data-cy="refresh"]').click()
cy.wait('@getStatusCompleted')
cy.get('[data-cy="status"]').should('contain', '完了')
})
})
カスタムコマンドと再利用可能なテストロジック
// cypress/support/commands.js
// カスタムコマンドの定義
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.visit('/login')
cy.get('[data-cy="username"]').type(username)
cy.get('[data-cy="password"]').type(password)
cy.get('[data-cy="login-button"]').click()
cy.url().should('include', '/dashboard')
})
})
Cypress.Commands.add('createTodo', (text) => {
cy.get('[data-cy="new-todo"]').type(`${text}{enter}`)
cy.get('[data-cy="todo-list"]').should('contain', text)
})
Cypress.Commands.add('deleteTodo', (text) => {
cy.contains('[data-cy="todo-item"]', text)
.find('[data-cy="delete-button"]')
.click()
cy.get('[data-cy="todo-list"]').should('not.contain', text)
})
// テストファイルでのカスタムコマンド使用
// cypress/e2e/todo-app.cy.js
describe('Todoアプリケーションテスト', () => {
beforeEach(() => {
cy.login('testuser', 'password123')
cy.visit('/todos')
})
it('Todoの作成と削除', () => {
// カスタムコマンドを使用
cy.createTodo('買い物に行く')
cy.createTodo('メールを返信する')
// Todo数の確認
cy.get('[data-cy="todo-item"]').should('have.length', 2)
// Todoの削除
cy.deleteTodo('買い物に行く')
cy.get('[data-cy="todo-item"]').should('have.length', 1)
})
it('Todoの編集とステータス変更', () => {
cy.createTodo('タスクを完了する')
// 編集機能
cy.contains('[data-cy="todo-item"]', 'タスクを完了する')
.find('[data-cy="edit-button"]')
.click()
cy.get('[data-cy="edit-input"]')
.clear()
.type('タスクを完了しました{enter}')
// ステータス変更
cy.contains('[data-cy="todo-item"]', 'タスクを完了しました')
.find('[data-cy="checkbox"]')
.check()
cy.get('[data-cy="completed-todos"]').should('contain', 'タスクを完了しました')
})
})
Cypress設定ファイル(cypress.config.js)
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
// ベースURL設定
baseUrl: 'http://localhost:3000',
// ビューポートサイズ
viewportWidth: 1280,
viewportHeight: 720,
// テストファイルの場所
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
// サポートファイルの場所
supportFile: 'cypress/support/e2e.js',
// タイムアウト設定
defaultCommandTimeout: 10000,
requestTimeout: 15000,
responseTimeout: 15000,
// 動画とスクリーンショット設定
video: true,
screenshotOnRunFailure: true,
// テスト結果の保存先
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
// テスト分離設定
testIsolation: true,
// 実験的機能
experimentalWebKitSupport: true,
setupNodeEvents(on, config) {
// プラグインの設定
on('task', {
// カスタムタスクの定義
log(message) {
console.log(message)
return null
},
// データベースリセット等のタスク
resetDb() {
// データベースリセット処理
return null
}
})
// ブラウザ起動時の設定
on('before:browser:launch', (browser = {}, launchOptions) => {
if (browser.family === 'chromium' && browser.name !== 'electron') {
// Chrome DevToolsの自動オープン
launchOptions.args.push('--auto-open-devtools-for-tabs')
}
return launchOptions
})
return config
},
},
component: {
// コンポーネントテストの設定
devServer: {
framework: 'react',
bundler: 'webpack',
},
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
},
// 環境変数
env: {
apiUrl: 'http://localhost:8080/api',
username: 'testuser',
password: 'password123',
},
})
CI/CD統合(GitHub Actions)
# .github/workflows/cypress.yml
name: Cypress Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, edge]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Start application
run: |
npm start &
npx wait-on http://localhost:3000
- name: Run Cypress tests
uses: cypress-io/github-action@v6
with:
browser: ${{ matrix.browser }}
record: true
parallel: true
group: 'E2E Tests - ${{ matrix.browser }}'
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots-${{ matrix.browser }}
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-videos-${{ matrix.browser }}
path: cypress/videos
デバッグとトラブルシューティング
// cypress/e2e/debug-example.cy.js
describe('デバッグ機能の活用', () => {
it('デバッグテクニックの例', () => {
cy.visit('/dashboard')
// ステップバイステップ実行(開発時のみ)
cy.pause() // 実行を一時停止してインスペクション可能
// 要素の詳細確認
cy.get('[data-cy="user-info"]').then(($el) => {
// 要素の詳細情報をコンソールに出力
console.log('Element:', $el[0])
console.log('Text content:', $el.text())
console.log('HTML:', $el.html())
})
// デバッグ用のスクリーンショット
cy.screenshot('debug-point-1')
// 条件付きデバッグ
cy.get('[data-cy="error-message"]').then(($error) => {
if ($error.is(':visible')) {
cy.screenshot('error-state')
cy.log('Error message is visible: ' + $error.text())
}
})
// カスタムログ出力
cy.task('log', 'テストのこの地点を通過しました')
// ブラウザコンソールの確認
cy.window().then((win) => {
console.log('Window object:', win)
console.log('Local storage:', win.localStorage)
})
// ネットワークリクエストの詳細確認
cy.intercept('GET', '/api/data').as('getData')
cy.get('[data-cy="load-data"]').click()
cy.wait('@getData').then((interception) => {
console.log('Request headers:', interception.request.headers)
console.log('Response body:', interception.response.body)
})
})
it('条件付きテスト実行', () => {
cy.visit('/feature-page')
// 要素の存在確認後の条件分岐
cy.get('body').then(($body) => {
if ($body.find('[data-cy="new-feature"]').length > 0) {
// 新機能が有効な場合のテスト
cy.get('[data-cy="new-feature"]').should('be.visible')
cy.get('[data-cy="new-feature"]').click()
} else {
// 旧機能のテスト
cy.get('[data-cy="old-feature"]').should('be.visible')
}
})
})
})