この記事の要点
• Cypressでブラウザ上のE2Eテストを作成・実行する
• cy.interceptでAPIをスタブしてテストの安定性を高める
• Custom CommandsとCI連携で実践的なテスト運用を行う
このチュートリアルで学ぶこと
- ✓ 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")を使いましょう。
よくあるエラーと対処
-
“cy.visit() failed”
- baseUrl と dev サーバが一致しているか確認
- サーバが起動済みか確認
-
フレイキーテスト (たまに落ちる)
- 固定の cy.wait(1000) を避け、cy.intercept + cy.wait(“@alias”) を使う
- アサーション (should) に頼ってCypressのリトライを活かす
-
要素が見つからない
- data-cy 属性を付与
- 親要素内で cy.within を活用
-
クロスドメイン
- cy.origin() で別ドメインのテストが可能 (OAuth等)
-
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経由のログインはテスト1件のみ通し、他のテストでは cy.request を使ってトークンを取得し
localStorage に格納する方法 (programmatic login) が推奨です。
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 Best Practices
- Cypress GitHub
- cypress-io/github-action
- Cypress Real World App (公式サンプル)
補足: 環境変数とテストデータ管理
実行環境ごとに異なる設定を扱うには、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化して時間短縮
- 長時間テストと短時間テストを別ジョブに分離
これによりフィードバックサイクルを数分以内に保てます。
← 一覧に戻る