Cypress E2Eテスト完全ガイド2026|コンポーネントテスト・CI連携・ベストプラクティス


Webアプリケーションの品質を担保するうえで、E2E(End-to-End)テストは欠かせない存在です。Cypressはブラウザ内で直接テストを実行するアーキテクチャにより、高速かつ安定したE2Eテストを実現するフレームワークとして広く採用されています。本記事では、2026年時点の最新バージョンをベースに、セットアップからCI/CD連携まで実践的に解説します。

Cypressとは

Cypressは、フロントエンド向けに設計されたJavaScriptベースのテストフレームワークです。ブラウザと同一プロセス内でテストコードが動作するため、DOMへの直接アクセスやネットワークリクエストのインターセプトが可能です。

主要フレームワークとの比較

項目CypressPlaywrightSelenium
対応ブラウザChrome, Firefox, Edge, ElectronChromium, Firefox, WebKit全主要ブラウザ
言語JavaScript / TypeScriptJS/TS, Python, Java, C#多言語対応
実行速度高速(ブラウザ内実行)高速(CDP/独自プロトコル)中程度(WebDriver経由)
並列実行Cypress Cloud(有料)/ 自前分割ネイティブサポートGrid構成が必要
コンポーネントテストネイティブサポート実験的サポート非対応
ネットワークモックcy.intercept(強力)route/page.route外部ライブラリ必要
デバッグ体験タイムトラベル・スナップショットTrace Viewerログベース
マルチタブ非対応対応対応
iframe操作プラグイン必要ネイティブ対応対応
学習コスト低い中程度高い

Cypressの最大の強みは、開発者体験(DX)の良さです。テスト実行中にブラウザ上でリアルタイムにDOMの状態を確認でき、各コマンドのスナップショットを遡って調べる「タイムトラベルデバッグ」が標準装備されています。

セットアップとプロジェクト構成

インストール

# npm
npm install -D cypress

# pnpm
pnpm add -D cypress

# yarn
yarn add -D cypress

初期化

npx cypress open

初回起動時にプロジェクトのテンプレートが生成されます。TypeScriptを使用する場合は、tsconfig.jsonが自動的にセットアップされます。

推奨ディレクトリ構成

cypress/
  e2e/                    # E2Eテストファイル
    auth/
      login.cy.ts
      signup.cy.ts
    dashboard/
      dashboard.cy.ts
  component/              # コンポーネントテスト
    Button.cy.tsx
    Modal.cy.tsx
  fixtures/               # テストデータ(JSON)
    users.json
    products.json
  support/
    commands.ts           # カスタムコマンド定義
    e2e.ts                # E2Eテストのグローバル設定
    component.ts          # コンポーネントテストのグローバル設定
  factories/              # テストデータファクトリ
    user.factory.ts
cypress.config.ts         # Cypress設定ファイル

設定ファイル

// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    retries: {
      runMode: 2,    // CI実行時のリトライ回数
      openMode: 0,   // GUI実行時はリトライしない
    },
    setupNodeEvents(on, config) {
      // プラグインの設定
    },
  },
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
    },
  },
});

TypeScript設定

// cypress/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["cypress"],
    "strict": true
  },
  "include": ["**/*.ts", "**/*.tsx"]
}

基本的なテストの書き方

最初のテスト

// cypress/e2e/home.cy.ts
describe('トップページ', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('ページタイトルが正しく表示される', () => {
    cy.title().should('include', 'MyApp');
  });

  it('ナビゲーションリンクが表示される', () => {
    cy.get('nav').within(() => {
      cy.contains('ホーム').should('be.visible');
      cy.contains('ダッシュボード').should('be.visible');
      cy.contains('設定').should('be.visible');
    });
  });

  it('検索フォームが機能する', () => {
    cy.get('[data-testid="search-input"]').type('Cypress');
    cy.get('[data-testid="search-button"]').click();
    cy.url().should('include', '?q=Cypress');
    cy.get('[data-testid="search-results"]')
      .should('exist')
      .children()
      .should('have.length.greaterThan', 0);
  });
});

コマンドチェーンとアサーション

Cypressのコマンドはチェーン形式で記述します。各コマンドは自動的にリトライされるため、明示的なwaitForは不要です。

// cypress/e2e/form.cy.ts
describe('お問い合わせフォーム', () => {
  it('バリデーションエラーが表示される', () => {
    cy.visit('/contact');

    // 空のまま送信
    cy.get('[data-testid="submit-button"]').click();

    // エラーメッセージの確認
    cy.get('[data-testid="error-name"]')
      .should('be.visible')
      .and('contain', '名前は必須です');

    cy.get('[data-testid="error-email"]')
      .should('be.visible')
      .and('contain', 'メールアドレスは必須です');
  });

  it('正常に送信できる', () => {
    cy.visit('/contact');

    cy.get('[data-testid="input-name"]').type('田中太郎');
    cy.get('[data-testid="input-email"]').type('tanaka@example.com');
    cy.get('[data-testid="input-message"]').type('テストメッセージです');
    cy.get('[data-testid="submit-button"]').click();

    cy.get('[data-testid="success-message"]')
      .should('be.visible')
      .and('contain', '送信が完了しました');
  });
});

セレクタ戦略(data-testid)

テストの安定性を左右する最も重要な要素がセレクタの選び方です。

セレクタの優先順位

// 推奨度: 高 → 低

// 1. data-testid(最も安定)
cy.get('[data-testid="login-button"]');

// 2. data-cy(Cypress専用属性)
cy.get('[data-cy="login-button"]');

// 3. アクセシビリティ属性(意味的に正しい)
cy.get('[role="dialog"]');
cy.get('[aria-label="閉じる"]');

// 4. テキストコンテンツ(変更されやすいが可読性が高い)
cy.contains('ログイン');

// 5. CSSクラス(非推奨: スタイル変更で壊れる)
cy.get('.btn-primary');  // 避ける

// 6. タグ + 構造(非推奨: DOM構造に依存)
cy.get('div > form > button');  // 避ける

コンポーネント側の実装例

// React コンポーネント
interface ButtonProps {
  label: string;
  onClick: () => void;
  testId?: string;
}

const Button: React.FC<ButtonProps> = ({ label, onClick, testId }) => (
  <button
    data-testid={testId}
    onClick={onClick}
    className="btn-primary"
  >
    {label}
  </button>
);

// 使用側
<Button
  label="ログイン"
  onClick={handleLogin}
  testId="login-button"
/>

カスタムセレクタの設定

data-testid属性を短く書けるようにカスタマイズします。

// cypress.config.ts
export default defineConfig({
  e2e: {
    // ...
  },
  // Cypress Testing Library のセレクタ設定
  env: {
    testIdAttribute: 'data-testid',
  },
});

APIモック(cy.intercept)

cy.interceptはネットワークリクエストをインターセプトし、レスポンスを差し替える強力な機能です。テストの安定性と速度を大幅に向上させます。

基本的なモック

// cypress/e2e/dashboard.cy.ts
describe('ダッシュボード', () => {
  it('ユーザー一覧を表示する', () => {
    // APIレスポンスをモック
    cy.intercept('GET', '/api/users', {
      statusCode: 200,
      body: [
        { id: 1, name: '田中太郎', email: 'tanaka@example.com' },
        { id: 2, name: '佐藤花子', email: 'sato@example.com' },
      ],
    }).as('getUsers');

    cy.visit('/dashboard');
    cy.wait('@getUsers');

    cy.get('[data-testid="user-list"]')
      .children()
      .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-testid="error-message"]')
      .should('be.visible')
      .and('contain', 'データの取得に失敗しました');
  });
});

条件付きインターセプト

describe('検索API', () => {
  it('検索結果をフィルタリングする', () => {
    cy.intercept('GET', '/api/search*', (req) => {
      const query = new URL(req.url).searchParams.get('q');

      if (query === 'react') {
        req.reply({
          statusCode: 200,
          body: {
            results: [
              { id: 1, title: 'React入門' },
              { id: 2, title: 'React Hooks完全ガイド' },
            ],
            total: 2,
          },
        });
      } else {
        req.reply({
          statusCode: 200,
          body: { results: [], total: 0 },
        });
      }
    }).as('search');

    cy.visit('/search');
    cy.get('[data-testid="search-input"]').type('react');
    cy.get('[data-testid="search-button"]').click();
    cy.wait('@search');

    cy.get('[data-testid="result-count"]').should('contain', '2件');
  });
});

リクエストの検証

it('正しいパラメータでAPIが呼ばれる', () => {
  cy.intercept('POST', '/api/orders', (req) => {
    // リクエストボディの検証
    expect(req.body).to.deep.include({
      productId: 'prod-001',
      quantity: 3,
    });
    expect(req.headers['content-type']).to.include('application/json');

    req.reply({ statusCode: 201, body: { orderId: 'order-123' } });
  }).as('createOrder');

  cy.visit('/products/prod-001');
  cy.get('[data-testid="quantity-input"]').clear().type('3');
  cy.get('[data-testid="order-button"]').click();
  cy.wait('@createOrder');
});

認証フローのテスト

認証が必要なページのテストでは、毎回ログインUIを操作するのは非効率です。APIを利用したプログラム的なログインを推奨します。

ログインコマンドの実装

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
      loginByApi(email: string, password: string): Chainable<void>;
    }
  }
}

// UI経由のログイン(ログインページ自体のテスト用)
Cypress.Commands.add('login', (email: string, password: string) => {
  cy.visit('/login');
  cy.get('[data-testid="email-input"]').type(email);
  cy.get('[data-testid="password-input"]').type(password);
  cy.get('[data-testid="login-button"]').click();
  cy.url().should('not.include', '/login');
});

// API経由のログイン(認証後ページのテスト用)
Cypress.Commands.add('loginByApi', (email: string, password: string) => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password },
  }).then((response) => {
    expect(response.status).to.eq(200);
    // トークンをローカルストレージに保存
    window.localStorage.setItem('authToken', response.body.token);
  });
});

export {};

認証テストの実装

// cypress/e2e/auth/login.cy.ts
describe('ログイン', () => {
  it('正しい認証情報でログインできる', () => {
    cy.login('user@example.com', 'password123');
    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="user-avatar"]').should('be.visible');
  });

  it('不正な認証情報でエラーが表示される', () => {
    cy.login('user@example.com', 'wrong-password');
    cy.get('[data-testid="login-error"]')
      .should('be.visible')
      .and('contain', 'メールアドレスまたはパスワードが正しくありません');
  });
});

// cypress/e2e/dashboard/dashboard.cy.ts
describe('ダッシュボード(認証済み)', () => {
  beforeEach(() => {
    // API経由で高速にログイン
    cy.loginByApi('user@example.com', 'password123');
  });

  it('ダッシュボードが表示される', () => {
    cy.visit('/dashboard');
    cy.get('[data-testid="dashboard-title"]')
      .should('contain', 'ダッシュボード');
  });
});

セッション管理の最適化

Cypress 12以降ではcy.sessionを使い、ログインセッションをキャッシュできます。

// cypress/support/commands.ts
Cypress.Commands.add('loginByApi', (email: string, password: string) => {
  cy.session(
    [email, password],
    () => {
      cy.request({
        method: 'POST',
        url: '/api/auth/login',
        body: { email, password },
      }).then((response) => {
        window.localStorage.setItem('authToken', response.body.token);
      });
    },
    {
      validate() {
        // セッションの有効性を確認
        cy.request({
          url: '/api/auth/me',
          headers: {
            Authorization: `Bearer ${window.localStorage.getItem('authToken')}`,
          },
        }).its('status').should('eq', 200);
      },
    }
  );
});

コンポーネントテスト

Cypress 12以降では、E2Eテストに加えてコンポーネント単体のテストも実行できます。アプリケーション全体を起動せずに、個々のコンポーネントを隔離してテストする手法です。

Reactコンポーネントのテスト

// cypress/component/TodoItem.cy.tsx
import TodoItem from '../../src/components/TodoItem';

describe('TodoItem', () => {
  it('タスク名が表示される', () => {
    cy.mount(
      <TodoItem
        todo={{ id: '1', title: '買い物', completed: false }}
        onToggle={cy.stub()}
        onDelete={cy.stub()}
      />
    );

    cy.get('[data-testid="todo-title"]').should('contain', '買い物');
  });

  it('完了チェックボックスをクリックするとonToggleが呼ばれる', () => {
    const onToggle = cy.stub().as('onToggle');

    cy.mount(
      <TodoItem
        todo={{ id: '1', title: '買い物', completed: false }}
        onToggle={onToggle}
        onDelete={cy.stub()}
      />
    );

    cy.get('[data-testid="todo-checkbox"]').click();
    cy.get('@onToggle').should('have.been.calledOnceWith', '1');
  });

  it('完了済みタスクには取り消し線が表示される', () => {
    cy.mount(
      <TodoItem
        todo={{ id: '1', title: '買い物', completed: true }}
        onToggle={cy.stub()}
        onDelete={cy.stub()}
      />
    );

    cy.get('[data-testid="todo-title"]')
      .should('have.css', 'text-decoration-line', 'line-through');
  });

  it('削除ボタンをクリックするとonDeleteが呼ばれる', () => {
    const onDelete = cy.stub().as('onDelete');

    cy.mount(
      <TodoItem
        todo={{ id: '1', title: '買い物', completed: false }}
        onToggle={cy.stub()}
        onDelete={onDelete}
      />
    );

    cy.get('[data-testid="todo-delete"]').click();
    cy.get('@onDelete').should('have.been.calledOnceWith', '1');
  });
});

フォームコンポーネントのテスト

// cypress/component/SearchForm.cy.tsx
import SearchForm from '../../src/components/SearchForm';

describe('SearchForm', () => {
  it('入力値を送信する', () => {
    const onSearch = cy.stub().as('onSearch');

    cy.mount(<SearchForm onSearch={onSearch} />);

    cy.get('[data-testid="search-input"]').type('Cypress テスト');
    cy.get('[data-testid="search-form"]').submit();

    cy.get('@onSearch').should('have.been.calledOnceWith', 'Cypress テスト');
  });

  it('空文字では送信できない', () => {
    const onSearch = cy.stub().as('onSearch');

    cy.mount(<SearchForm onSearch={onSearch} />);

    cy.get('[data-testid="search-form"]').submit();

    cy.get('@onSearch').should('not.have.been.called');
    cy.get('[data-testid="search-error"]')
      .should('contain', '検索キーワードを入力してください');
  });
});

カスタムコマンド

テストコード内で繰り返し使用する操作は、カスタムコマンドとして抽出しましょう。

定義と型安全性

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      /** data-testid属性でエレメントを取得する */
      getByTestId(testId: string): Chainable<JQuery<HTMLElement>>;
      /** テーブルの行数を検証する */
      tableRowCount(testId: string, count: number): Chainable<void>;
      /** トースト通知の表示を検証する */
      shouldShowToast(message: string, type?: 'success' | 'error' | 'info'): Chainable<void>;
      /** ドラッグ&ドロップ操作 */
      dragTo(targetSelector: string): Chainable<void>;
    }
  }
}

Cypress.Commands.add('getByTestId', (testId: string) => {
  return cy.get(`[data-testid="${testId}"]`);
});

Cypress.Commands.add('tableRowCount', (testId: string, count: number) => {
  cy.getByTestId(testId)
    .find('tbody tr')
    .should('have.length', count);
});

Cypress.Commands.add('shouldShowToast', (message: string, type = 'success') => {
  cy.get(`[data-testid="toast-${type}"]`)
    .should('be.visible')
    .and('contain', message);

  // 自動消去を待つ
  cy.get(`[data-testid="toast-${type}"]`, { timeout: 6000 })
    .should('not.exist');
});

Cypress.Commands.add(
  'dragTo',
  { prevSubject: 'element' },
  (subject: JQuery<HTMLElement>, targetSelector: string) => {
    cy.wrap(subject).trigger('dragstart');
    cy.get(targetSelector).trigger('drop');
    cy.wrap(subject).trigger('dragend');
  }
);

export {};

カスタムコマンドの使用例

// cypress/e2e/users.cy.ts
describe('ユーザー管理', () => {
  beforeEach(() => {
    cy.loginByApi('admin@example.com', 'admin123');
    cy.visit('/admin/users');
  });

  it('ユーザーテーブルに10件表示される', () => {
    cy.tableRowCount('user-table', 10);
  });

  it('ユーザー削除後にトースト通知が表示される', () => {
    cy.getByTestId('delete-user-1').click();
    cy.getByTestId('confirm-delete').click();
    cy.shouldShowToast('ユーザーを削除しました', 'success');
  });
});

フィクスチャとファクトリ

テストデータの管理はテストの保守性に直結します。フィクスチャとファクトリを適切に使い分けましょう。

フィクスチャ(静的データ)

// cypress/fixtures/users.json
{
  "admin": {
    "id": "user-001",
    "name": "管理者太郎",
    "email": "admin@example.com",
    "role": "admin"
  },
  "member": {
    "id": "user-002",
    "name": "一般花子",
    "email": "member@example.com",
    "role": "member"
  }
}
// フィクスチャの使用
describe('ユーザープロフィール', () => {
  it('管理者情報を表示する', () => {
    cy.fixture('users').then((users) => {
      cy.intercept('GET', '/api/users/me', users.admin).as('getMe');
      cy.visit('/profile');
      cy.wait('@getMe');
      cy.getByTestId('user-name').should('contain', '管理者太郎');
      cy.getByTestId('user-role').should('contain', '管理者');
    });
  });
});

ファクトリ(動的データ生成)

// cypress/factories/user.factory.ts
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'member' | 'viewer';
  createdAt: string;
}

let userCounter = 0;

export function createUser(overrides: Partial<User> = {}): User {
  userCounter++;
  return {
    id: `user-${String(userCounter).padStart(3, '0')}`,
    name: `テストユーザー${userCounter}`,
    email: `user${userCounter}@example.com`,
    role: 'member',
    createdAt: new Date().toISOString(),
    ...overrides,
  };
}

export function createUsers(count: number, overrides: Partial<User> = {}): User[] {
  return Array.from({ length: count }, () => createUser(overrides));
}
// ファクトリの使用
import { createUser, createUsers } from '../factories/user.factory';

describe('ユーザー一覧', () => {
  it('ページネーションが正しく動作する', () => {
    const users = createUsers(25);

    cy.intercept('GET', '/api/users?page=1', {
      body: { data: users.slice(0, 10), total: 25 },
    }).as('page1');

    cy.intercept('GET', '/api/users?page=2', {
      body: { data: users.slice(10, 20), total: 25 },
    }).as('page2');

    cy.visit('/admin/users');
    cy.wait('@page1');
    cy.tableRowCount('user-table', 10);

    cy.getByTestId('pagination-next').click();
    cy.wait('@page2');
    cy.tableRowCount('user-table', 10);
  });

  it('管理者ユーザーにはバッジが表示される', () => {
    const admin = createUser({ role: 'admin', name: '管理者' });
    const member = createUser({ role: 'member', name: '一般ユーザー' });

    cy.intercept('GET', '/api/users*', {
      body: { data: [admin, member], total: 2 },
    });

    cy.visit('/admin/users');
    cy.contains('管理者').parent().find('[data-testid="admin-badge"]').should('exist');
    cy.contains('一般ユーザー').parent().find('[data-testid="admin-badge"]').should('not.exist');
  });
});

CI/CD連携(GitHub Actions)

CypressテストをCI環境で自動実行する設定です。

GitHub Actionsワークフロー

# .github/workflows/e2e.yml
name: E2E Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        containers: [1, 2, 3]  # 3並列で実行

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build

      - name: Cypress run
        uses: cypress-io/github-action@v6
        with:
          start: npm run preview
          wait-on: 'http://localhost:4173'
          wait-on-timeout: 120
          browser: chrome
          record: true
          parallel: true
          group: 'e2e-chrome'
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload screenshots on failure
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: cypress-screenshots-${{ matrix.containers }}
          path: cypress/screenshots
          retention-days: 7

      - name: Upload videos
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: cypress-videos-${{ matrix.containers }}
          path: cypress/videos
          retention-days: 3

  component:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'
      - run: npm ci
      - name: Component tests
        uses: cypress-io/github-action@v6
        with:
          component: true
          browser: chrome

テスト結果のレポート

// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    // CI環境でのスクリーンショット・動画設定
    screenshotsFolder: 'cypress/screenshots',
    videosFolder: 'cypress/videos',
    video: true,
    screenshotOnRunFailure: true,
    // JUnitレポーター(CI連携用)
    reporter: 'junit',
    reporterOptions: {
      mochaFile: 'cypress/results/results-[hash].xml',
      toConsole: false,
    },
  },
});

Cypress Cloud

Cypress Cloudは、テスト結果の可視化・並列実行・フレーキーテスト検出を提供するSaaSサービスです。

主な機能

  • テスト分析ダッシュボード: 実行時間、成功率、フレーキーテストの傾向をグラフで確認
  • スマート並列実行: テストの実行時間に基づき、複数CIマシンに最適分配
  • フレーキーテスト検出: 不安定なテストを自動特定し、優先修正候補を提示
  • テスト再生: 失敗時のスクリーンショット・動画・DOMスナップショットを保存

セットアップ

# プロジェクトIDの取得
npx cypress open
# Cypress Cloud タブから "Connect to Cypress Cloud" を選択
# プロジェクトIDとレコードキーが発行される
// cypress.config.ts
export default defineConfig({
  projectId: 'your-project-id',  // Cypress Cloudのプロジェクト識別子
  e2e: {
    // ...
  },
});
# レコードキー付きで実行
npx cypress run --record --key YOUR_RECORD_KEY

無料プランの制限

Cypress Cloudの無料プラン(Starter)では月間500テスト結果まで記録可能です。小規模プロジェクトやOSSでは十分に活用できますが、CIの実行回数が多い場合は有料プラン(Team以上)の検討が必要になります。

無料の代替手段

Cypress Cloudを使わずに並列実行を実現する方法もあります。

# specパターンで手動分割する例
jobs:
  e2e-auth:
    steps:
      - uses: cypress-io/github-action@v6
        with:
          spec: 'cypress/e2e/auth/**/*.cy.ts'

  e2e-dashboard:
    steps:
      - uses: cypress-io/github-action@v6
        with:
          spec: 'cypress/e2e/dashboard/**/*.cy.ts'

Playwrightとの使い分け

CypressとPlaywrightはそれぞれ異なる強みを持っています。プロジェクトの要件に応じて最適なツールを選択しましょう。

Cypressが適しているケース

  • フロントエンド中心のSPA/SSRアプリ: Reactの状態やDOMを直接操作・検証できる
  • コンポーネントテストとE2Eを統一したい: 同一フレームワークでコンポーネントテストとE2Eテストの両方を実行可能
  • チーム内にテスト経験が少ないメンバーがいる: タイムトラベルデバッグやインタラクティブなGUIが学習をサポート
  • APIモックを多用する: cy.interceptの直感的なAPIが強力

Playwrightが適しているケース

  • マルチブラウザ対応が必須: WebKit(Safari相当)でのテストが必要
  • マルチタブ・マルチウィンドウ操作: OAuth認証のポップアップ、新規タブでの遷移など
  • iframeの複雑な操作: 決済フォームや埋め込みウィジェットのテスト
  • 大規模なテストスイート: ネイティブの並列実行により高速に処理
  • Python/Java/C#チーム: JavaScript以外の言語でテストを書きたい場合

併用パターン

実際のプロジェクトでは両方を併用するケースもあります。

テスト戦略
  コンポーネントテスト → Cypress Component Testing
  E2Eテスト(主要フロー) → Cypress
  クロスブラウザテスト → Playwright
  ビジュアルリグレッション → Playwright + Percy/Argos

判断フローチャート

マルチタブ・iframe操作が必要?
  → Yes → Playwright
  → No
    コンポーネントテストも統一したい?
      → Yes → Cypress
      → No
        WebKit対応が必須?
          → Yes → Playwright
          → No
            チームのJS習熟度が低い?
              → Yes → Cypress(DXが優れる)
              → No → どちらでも可(好みで選択)

まとめ

Cypressは2026年現在も、フロントエンドE2Eテストの定番ツールとして進化を続けています。本記事で解説した内容を振り返ります。

  • セットアップ: npm install -D cypressで導入し、cypress.config.tsでTypeScript環境を構築
  • セレクタ戦略: data-testidを基本とし、DOM構造やCSSクラスへの依存を排除
  • APIモック: cy.interceptでバックエンドに依存しない安定したテストを実現
  • 認証テスト: cy.sessionでログインセッションをキャッシュし、テスト速度を最適化
  • コンポーネントテスト: E2Eと同じフレームワークでコンポーネント単体の検証が可能
  • カスタムコマンド: 型安全なカスタムコマンドで重複を排除し、保守性を向上
  • CI/CD: GitHub Actionsとの連携により、PR単位での自動テスト実行を構築
  • Playwrightとの比較: 要件に応じた使い分けが重要。併用も有効な選択肢

テストは書くこと自体が目的ではなく、アプリケーションの品質に対する信頼性を継続的に担保する仕組みです。まずは主要なユーザーフローからE2Eテストを導入し、段階的にカバレッジを広げていくことをおすすめします。

関連記事