テストツール

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インストール
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')
      }
    })
  })
})