Cypress実践ガイド - フロントエンドE2Eテスト入門

intermediate | 60分 で読める | 2026.04.10

公式ドキュメント

このチュートリアルで学ぶこと

  • ✓ Cypressのインストールと初期設定
  • ✓ 基本的なコマンドとセレクタ戦略
  • ✓ フォーム / APIモック / ログインフロー
  • ✓ fixture / custom commands
  • ✓ ビジュアル確認と失敗時のデバッグ
  • ✓ GitHub Actions での実行

前提条件

  • JavaScript / TypeScript の基本
  • Node.js 20 以上
  • 対象となるWebアプリ (本チュートリアルではVite + Reactを例に)
  • HTMLセレクタと DOM の基礎

プロジェクトのセットアップ

# 既存のフロントエンドプロジェクトで
npm install -D cypress

# 初回起動 (Test Runner GUIが開く)
npx cypress open

初回起動すると cypress/ ディレクトリと cypress.config.ts が生成されます。

ディレクトリ構造

project/
├── cypress/
│ ├── e2e/
│ │ └── spec.cy.ts
│ ├── fixtures/
│ │ └── users.json
│ ├── support/
│ │ ├── commands.ts
│ │ └── e2e.ts
│ └── downloads/
├── cypress.config.ts
└── package.json

基本概念

CypressはElectronベースのブラウザで実際にアプリを動かしながらテストを実行します。 cy グローバルオブジェクトのコマンドはキューに積まれ、順番に非同期実行されます (Promiseではなくchainable)。

cy.visit → cy.get → cy.type → cy.click → cy.should
 ────── キューで順次実行 ──────

実践メモ: 初回のnpx cypress openで自動的にディレクトリ構造と設定ファイルが生成されます。手動で作る必要はありません。

Step 1: 設定ファイル

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

export default defineConfig({
 e2e: {
 baseUrl: "http://localhost:5173",
 viewportWidth: 1280,
 viewportHeight: 800,
 video: false,
 screenshotOnRunFailure: true,
 setupNodeEvents(on, config) {
 return config;
 },
 },
});

Step 2: 最初のテスト

// cypress/e2e/home.cy.ts
describe("ホームページ", () => {
 beforeEach(() => {
 cy.visit("/");
 });

 it("タイトルが表示される", () => {
 cy.get("h1").should("contain.text", "ようこそ");
 });

 it("ナビゲーションリンクが機能する", () => {
 cy.get('[data-cy="nav-about"]').click();
 cy.url().should("include", "/about");
 cy.get("h1").should("contain.text", "About");
 });
});

セレクタ戦略

ポイント: data-cy属性を使うことで、スタイル変更やリファクタリングの影響を受けない安定したセレクタになります。

/* 良い例 */
[data-cy="submit-button"]

/* 悪い例 */
.btn-primary /* スタイル変更で壊れる */
#id /* 動的生成されやすい */

Cypress公式は data-cy 属性の付与を推奨しています。

Step 3: フォームの入力と検証

// cypress/e2e/signup.cy.ts
describe("サインアップ", () => {
 beforeEach(() => cy.visit("/signup"));

 it("必須項目が空だとエラーが出る", () => {
 cy.get('[data-cy="submit"]').click();
 cy.get('[data-cy="error-email"]').should("be.visible");
 });

 it("正常に登録できる", () => {
 cy.get('[data-cy="input-name"]').type("山田太郎");
 cy.get('[data-cy="input-email"]').type("taro@example.com");
 cy.get('[data-cy="input-password"]').type("secret123");
 cy.get('[data-cy="submit"]').click();

 cy.url().should("include", "/welcome");
 cy.contains("山田太郎さん、ようこそ");
 });
});

注意: cy.typeはデフォルトで一文字ずつ入力します。大量テキストの場合は{literal_text}を検討してください。

Step 4: ネットワークのスタブとインターセプト

// cypress/e2e/posts.cy.ts
describe("記事一覧", () => {
 it("APIレスポンスをスタブする", () => {
 cy.intercept("GET", "/api/posts", {
 fixture: "posts.json",
 }).as("getPosts");

 cy.visit("/posts");
 cy.wait("@getPosts");

 cy.get('[data-cy="post-item"]').should("have.length", 3);
 });

 it("エラー時の表示を検証", () => {
 cy.intercept("GET", "/api/posts", {
 statusCode: 500,
 body: { message: "Server Error" },
 }).as("getPostsError");

 cy.visit("/posts");
 cy.wait("@getPostsError");
 cy.get('[data-cy="error-banner"]').should("be.visible");
 });
});
// cypress/fixtures/posts.json
[
 { "id": 1, "title": "Cypress入門" },
 { "id": 2, "title": "セレクタ戦略" },
 { "id": 3, "title": "CI連携" }
]

実践メモ: fixtureでAPIレスポンスを固定すると、外部サービスに依存しない高速で安定したテストになります。

Step 5: Custom Commands (ログインの共通化)

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

Cypress.Commands.add("login", (email, password) => {
 cy.visit("/login");
 cy.get('[data-cy="email"]').type(email);
 cy.get('[data-cy="password"]').type(password);
 cy.get('[data-cy="submit"]').click();
 cy.url().should("not.include", "/login");
});

Cypress.Commands.add("loginByApi", (email, password) => {
 cy.request("POST", "/api/login", { email, password }).then((res) => {
 window.localStorage.setItem("token", res.body.token);
 });
});

export {};
// cypress/support/e2e.ts
import "./commands";
// 使用例
describe("ダッシュボード", () => {
 beforeEach(() => {
 cy.loginByApi("test@example.com", "password");
 cy.visit("/dashboard");
 });

 it("ユーザー名が表示される", () => {
 cy.get('[data-cy="user-name"]').should("be.visible");
 });
});

Step 6: 非同期と再試行

Cypressのコマンドはデフォルトで4秒間リトライします。shouldはアサーション再試行の対象となる重要なコマンドです。

// 自動リトライされる
cy.get('[data-cy="toast"]', { timeout: 10000 })
 .should("contain.text", "保存しました");

// 非同期状態の安定化
cy.intercept("POST", "/api/save").as("save");
cy.get('[data-cy="save"]').click();
cy.wait("@save").its("response.statusCode").should("eq", 200);

完成コード: ログイン〜投稿作成フロー

// cypress/e2e/create-post.cy.ts
describe("記事作成フロー", () => {
 beforeEach(() => {
 cy.intercept("GET", "/api/me", { fixture: "me.json" }).as("me");
 cy.intercept("POST", "/api/posts", {
 statusCode: 201,
 body: { id: 100, title: "新規記事", body: "本文" },
 }).as("createPost");

 cy.loginByApi("author@example.com", "password");
 cy.visit("/posts/new");
 cy.wait("@me");
 });

 it("記事を作成して一覧へ遷移する", () => {
 cy.get('[data-cy="title"]').type("新規記事");
 cy.get('[data-cy="body"]').type("本文のテキストです");
 cy.get('[data-cy="submit"]').click();

 cy.wait("@createPost").its("request.body").should("deep.include", {
 title: "新規記事",
 });

 cy.url().should("match", /\/posts\/100$/);
 cy.contains("新規記事");
 });

 it("バリデーションエラー", () => {
 cy.get('[data-cy="submit"]').click();
 cy.get('[data-cy="error-title"]').should("be.visible");
 });
});

GitHub Actions での実行

# .github/workflows/e2e.yml
name: E2E
on: [push, pull_request]

jobs:
 cypress-run:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: 20
 cache: npm
 - run: npm ci
 - run: npm run build
 - name: Cypress run
 uses: cypress-io/github-action@v6
 with:
 start: npm run preview
 wait-on: "http://localhost:4173"
 browser: chrome

注意: 固定のcy.wait(1000)はフレイキーテストの原因になります。代わりにcy.intercept+cy.wait("@alias")を使いましょう。

よくあるエラーと対処

  1. “cy.visit() failed”
  • baseUrl と dev サーバが一致しているか確認
  • サーバが起動済みか確認
  1. フレイキーテスト (たまに落ちる)
  • 固定の cy.wait(1000) を避け、cy.intercept + cy.wait(“@alias”) を使う
  • アサーション (should) に頼ってCypressのリトライを活かす
  1. 要素が見つからない
  • data-cy 属性を付与
  • 親要素内で cy.within を活用
  1. クロスドメイン
  • cy.origin() で別ドメインのテストが可能 (OAuth等)
  1. CI上でスクリーンショットが保存されない
  • actions/upload-artifact でアップロード

ベストプラクティス

  • data-cy 属性でセレクタを安定化
  • テスト間で状態を残さない (beforeEach でリセット)
  • ログインはAPIで直接行い、UIテストではない部分を高速化
  • 1テスト = 1シナリオ
  • fixture でレスポンスを固定し、外部依存を減らす
  • 失敗時にスクリーンショット/ビデオを残す

次のステップ

  • Component Testing (コンポーネント単位のテスト)
  • Cypress Cloud (並列実行と録画)
  • アクセシビリティテスト (cypress-axe)
  • Percy連携でのビジュアルリグレッション
  • Playwrightとの使い分け検討

ここまでの整理

Cypressは「テストコードが実際のユーザー操作に近い」という分かりやすさが最大の魅力です。 自動リトライ、ネットワークスタブ、リッチなTest Runnerにより、フレイキーさを抑えながら UIの品質を維持できます。CIとの統合も容易で、小〜中規模のWebアプリに特によく合います。

FAQ

Q. Cypress と Playwright の違いは? A. Cypressはブラウザ内でテストを実行するアーキテクチャで、開発者体験 (GUI Test Runner) が秀逸です。 Playwrightは複数ブラウザ・複数言語対応、並列実行性能に強みがあります。用途に応じて使い分けましょう。

Q. モバイルアプリのテストはできる? A. ネイティブアプリはサポート外ですが、レスポンシブWebは cy.viewport() でサイズを切り替えられます。

Q. 認証フローはどう扱う? A. UI経由のログインを確認するテストと、cy.request()などで認証を準備するテストを分けると効率的です。保存先は実際のアプリ仕様に合わせます。HttpOnly Cookieを使う設計なら、テスト都合でトークンをlocalStorageへ変更しないでください。

Q. テストが遅い場合は? A. Cypress Cloud で並列実行、もしくはCIで複数コンテナに分割して実行します。 1テスト内の不要な cy.wait を削り、APIスタブで外部依存を切ることも有効です。

チートシート

// Cypress 13.x
// https://docs.cypress.io/

cy.visit(url) // ページ遷移
cy.get(selector) // 要素取得 (自動リトライ)
cy.contains(text) // テキストで要素取得
cy.intercept(method, url, handler) // ネットワークをスタブ / 監視
cy.wait("@alias") // インターセプト完了待ち
cy.fixture(name) // フィクスチャ読込
cy.request(options) // 直接HTTPリクエスト
cy.viewport(w, h) // ビューポート変更
.should("be.visible") // アサーション
.then((value) => {...}) // 同期処理への接続

まとめ

この記事では、手順を追って動かしながら重要な確認ポイントを押さえることが大切です。まずは学習用の最小構成で動作確認し、本番利用する前にセキュリティ、権限、エラー処理、運用手順を見直してください。

参考リソース

補足: 環境変数とテストデータ管理

実行環境ごとに異なる設定を扱うには、Cypressの環境変数機能を使います。

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

export default defineConfig({
 e2e: {
 baseUrl: process.env.CYPRESS_BASE_URL ?? "http://localhost:5173",
 env: {
 apiUrl: process.env.CYPRESS_API_URL ?? "http://localhost:3000/api",
 adminEmail: "admin@example.com",
 },
 },
});

テスト内で取得:

const apiUrl = Cypress.env("apiUrl");
cy.request(`${apiUrl}/health`).its("status").should("eq", 200);

データベース状態のリセット

E2Eは実データに依存しがちなので、テスト開始時に状態を初期化するタスクを用意します。

// cypress.config.ts 内の setupNodeEvents
on("task", {
 async resetDb() {
 // DBを truncate / seed するロジック
 return null;
 },
});
// テストコード
before(() => {
 cy.task("resetDb");
});

ポイント: テスト間の独立性を保つことで、フレイキーさを大きく減らせます。cy.task("resetDb")パターンを活用しましょう。

並列実行のヒント

CIでテスト数が増えてきたら、以下を検討します。

  • spec単位でジョブを分割して複数ランナーで実行
  • Cypress Cloud を利用して自動分散
  • 共通の前処理 (ログイン等) をAPI化して時間短縮
  • 長時間テストと短時間テストを別ジョブに分離

これによりフィードバックサイクルを数分以内に保てます。

← 一覧に戻る
PR
PR
PR
PR