Day 2: はじめてのテストを書こう
今日学ぶこと
- テストファイルの基本構造(describe, it, before, beforeEach)
- cy.visit() でページを開く方法
- cy.get() で要素を取得する方法
- cy.contains() でテキストを検索する方法
- 基本的なアサーション(should, expect)
- テストの実行方法(cypress open, cypress run)
- 実践:TodoアプリのE2Eテスト
テストファイルの基本構造
Cypressのテストファイルは cypress/e2e/ ディレクトリに配置し、拡張子は .cy.js(または .cy.ts)とします。
// cypress/e2e/sample.cy.js
describe('テストスイートの名前', () => {
before(() => {
// テストスイート全体の前に1回だけ実行される
})
beforeEach(() => {
// 各テストの前に毎回実行される
})
afterEach(() => {
// 各テストの後に毎回実行される
})
after(() => {
// テストスイート全体の後に1回だけ実行される
})
it('テストケースの名前', () => {
// テストコードをここに書く
})
it('別のテストケース', () => {
// 別のテストコード
})
})
各ブロックの役割
flowchart TB
subgraph Lifecycle["テストのライフサイクル"]
B["before()\n1回だけ実行"]
BE1["beforeEach()\n毎回実行"]
T1["it('テスト1')"]
AE1["afterEach()\n毎回実行"]
BE2["beforeEach()\n毎回実行"]
T2["it('テスト2')"]
AE2["afterEach()\n毎回実行"]
A["after()\n1回だけ実行"]
end
B --> BE1 --> T1 --> AE1 --> BE2 --> T2 --> AE2 --> A
style B fill:#8b5cf6,color:#fff
style A fill:#8b5cf6,color:#fff
style BE1 fill:#3b82f6,color:#fff
style BE2 fill:#3b82f6,color:#fff
style AE1 fill:#f59e0b,color:#fff
style AE2 fill:#f59e0b,color:#fff
style T1 fill:#22c55e,color:#fff
style T2 fill:#22c55e,color:#fff
| ブロック | 実行タイミング | 主な用途 |
|---|---|---|
before() |
テストスイートの最初に1回 | データベースの初期化、共通データの準備 |
beforeEach() |
各テストの前に毎回 | ページへの遷移、状態のリセット |
it() |
テスト本体 | 実際のテストロジック |
afterEach() |
各テストの後に毎回 | クリーンアップ処理 |
after() |
テストスイートの最後に1回 | 全体のクリーンアップ |
describeのネスト
テストを論理的にグループ化するために、describe をネストすることもできます。
describe('ユーザー管理', () => {
describe('ログイン', () => {
it('正しい認証情報でログインできる', () => {
// ...
})
it('間違ったパスワードでエラーが表示される', () => {
// ...
})
})
describe('ユーザー登録', () => {
it('新規ユーザーを作成できる', () => {
// ...
})
})
})
cy.visit() でページを開く
テストの最初のステップは、テスト対象のページを開くことです。cy.visit() を使います。
// 絶対URLで指定
cy.visit('http://localhost:3000')
// baseUrlが設定されていれば、相対パスで指定できる
// cypress.config.js で baseUrl: 'http://localhost:3000' の場合
cy.visit('/') // http://localhost:3000/
cy.visit('/about') // http://localhost:3000/about
cy.visit('/login') // http://localhost:3000/login
visit() のオプション
cy.visit('/', {
// タイムアウト(ミリ秒)
timeout: 30000,
// 失敗時のステータスコードを許容
failOnStatusCode: false,
// Basic認証
auth: {
username: 'admin',
password: 'password123',
},
// クエリパラメータ
qs: {
page: 1,
sort: 'name',
},
})
よくあるパターン:beforeEachでページを開く
describe('トップページ', () => {
beforeEach(() => {
// 各テストの前にトップページを開く
cy.visit('/')
})
it('タイトルが表示される', () => {
// ページが既に開かれている状態でテスト
})
it('ナビゲーションが表示される', () => {
// ページが既に開かれている状態でテスト
})
})
cy.get() で要素を取得
cy.get() はCSSセレクタを使ってDOM要素を取得するコマンドです。Cypressで最も頻繁に使うコマンドの1つです。
// IDで取得
cy.get('#submit-button')
// クラス名で取得
cy.get('.nav-link')
// タグ名で取得
cy.get('h1')
// 属性で取得
cy.get('[type="email"]')
// data属性で取得(推奨)
cy.get('[data-testid="login-form"]')
// 複合セレクタ
cy.get('form.login-form input[type="email"]')
data-testid を使う理由
flowchart TB
subgraph Bad["避けるべきセレクタ"]
B1["cy.get('.btn-primary')\nクラス名はスタイル変更で壊れる"]
B2["cy.get('#main > div:nth-child(2)')\nDOM構造の変更で壊れる"]
B3["cy.get('button')\n曖昧すぎる"]
end
subgraph Good["推奨セレクタ"]
G1["cy.get('[data-testid=submit]')\nテスト専用の属性"]
G2["cy.get('[data-cy=submit]')\nCypress推奨の属性"]
end
style Bad fill:#ef4444,color:#fff
style Good fill:#22c55e,color:#fff
| セレクタの種類 | 安定性 | 理由 |
|---|---|---|
data-testid / data-cy |
高い | テスト専用。開発者が意図的に変更しない限り壊れない |
id |
中程度 | 一意だが、リファクタリングで変更される可能性がある |
class |
低い | CSSの変更で簡単に壊れる |
| DOM構造 | 非常に低い | UIの変更で頻繁に壊れる |
要素の操作
取得した要素に対して、さまざまな操作を行えます。
// クリック
cy.get('[data-testid="submit-btn"]').click()
// テキスト入力
cy.get('[data-testid="email-input"]').type('user@example.com')
// 入力値をクリア
cy.get('[data-testid="email-input"]').clear()
// クリアしてから入力
cy.get('[data-testid="email-input"]').clear().type('new@example.com')
// セレクトボックスの選択
cy.get('[data-testid="country-select"]').select('Japan')
// チェックボックスのチェック
cy.get('[data-testid="agree-checkbox"]').check()
// チェックボックスのチェック解除
cy.get('[data-testid="agree-checkbox"]').uncheck()
cy.contains() でテキストを検索
cy.contains() は、指定したテキストを含む要素を検索します。テキストベースでの要素取得に便利です。
// テキストを含む要素を取得
cy.contains('ログイン')
// 特定のタグ内でテキストを検索
cy.contains('button', 'ログイン')
// 正規表現も使える
cy.contains(/^ようこそ/)
// 取得した要素をクリック
cy.contains('送信').click()
cy.get() と cy.contains() の使い分け
flowchart LR
subgraph GetCmd["cy.get()"]
G["CSSセレクタで\n要素を特定"]
end
subgraph ContainsCmd["cy.contains()"]
C["テキスト内容で\n要素を特定"]
end
subgraph UseCase["使い分け"]
U1["フォーム入力欄 → cy.get()"]
U2["ボタンのラベル → cy.contains()"]
U3["エラーメッセージ → cy.contains()"]
U4["特定のdata属性 → cy.get()"]
end
style GetCmd fill:#3b82f6,color:#fff
style ContainsCmd fill:#8b5cf6,color:#fff
style UseCase fill:#22c55e,color:#fff
| メソッド | 使うべき場面 | 例 |
|---|---|---|
cy.get() |
特定の属性やセレクタで要素を指定したい | 入力フォーム、特定のコンポーネント |
cy.contains() |
テキスト内容で要素を見つけたい | ボタン、メッセージ、リンク |
基本的なアサーション
アサーションは「テストの期待値を検証する」ための仕組みです。Cypressでは主に .should() を使います。
should() によるアサーション
// 要素が表示されていることを確認
cy.get('[data-testid="welcome"]').should('be.visible')
// 要素が存在することを確認
cy.get('[data-testid="header"]').should('exist')
// 要素が存在しないことを確認
cy.get('[data-testid="error"]').should('not.exist')
// テキスト内容を確認
cy.get('h1').should('have.text', 'ようこそ')
// テキストを含むことを確認
cy.get('.message').should('contain', '成功')
// CSSクラスを持つことを確認
cy.get('.alert').should('have.class', 'alert-success')
// 属性の値を確認
cy.get('a').should('have.attr', 'href', '/about')
// 入力値を確認
cy.get('input').should('have.value', 'test@example.com')
// 要素の数を確認
cy.get('li').should('have.length', 5)
// 無効化されていることを確認
cy.get('button').should('be.disabled')
// 有効化されていることを確認
cy.get('button').should('not.be.disabled')
should() のチェーン
複数のアサーションをチェーンすることもできます。
cy.get('[data-testid="status"]')
.should('be.visible')
.and('have.text', '完了')
.and('have.class', 'status-complete')
.and('have.css', 'color', 'rgb(34, 197, 94)')
主要なアサーション一覧
| アサーション | 説明 | 例 |
|---|---|---|
be.visible |
要素が表示されている | .should('be.visible') |
exist |
要素がDOMに存在する | .should('exist') |
have.text |
テキストが完全一致 | .should('have.text', 'Hello') |
contain |
テキストを含む | .should('contain', 'Hello') |
have.value |
入力値が一致 | .should('have.value', 'test') |
have.length |
要素数が一致 | .should('have.length', 3) |
have.class |
クラスを持つ | .should('have.class', 'active') |
have.attr |
属性を持つ | .should('have.attr', 'href') |
be.disabled |
無効化されている | .should('be.disabled') |
be.checked |
チェックされている | .should('be.checked') |
テストの実行方法
Cypressにはテストを実行する2つの方法があります。
flowchart LR
subgraph Open["cypress open(GUI)"]
O1["ブラウザ選択"]
O2["テストファイル選択"]
O3["リアルタイム実行"]
O4["タイムトラベル\nデバッグ"]
end
subgraph Run["cypress run(CLI)"]
R1["ヘッドレス実行"]
R2["全テスト自動実行"]
R3["CI/CD向け"]
R4["動画・スクリーンショット\n自動保存"]
end
style Open fill:#3b82f6,color:#fff
style Run fill:#8b5cf6,color:#fff
cypress open(インタラクティブモード)
# GUIを起動
npx cypress open
- 開発中にテストを書きながら実行するのに最適
- テストの各ステップをスナップショットで確認(タイムトラベル)
- テストファイルの変更を検知して自動再実行
cypress run(ヘッドレスモード)
# 全テストを実行
npx cypress run
# 特定のファイルのみ実行
npx cypress run --spec "cypress/e2e/todo.cy.js"
# ブラウザを指定
npx cypress run --browser chrome
# 動画を記録
npx cypress run --config video=true
- CI/CDパイプラインでの実行に最適
- コマンドラインで完結し、GUIは不要
- テスト結果をターミナルに出力
package.json にスクリプトを追加
// package.json
{
"scripts": {
"cy:open": "cypress open",
"cy:run": "cypress run",
"cy:run:chrome": "cypress run --browser chrome"
}
}
実践:TodoアプリのE2Eテスト
ここまで学んだ知識を使って、TodoアプリのE2Eテストを書いてみましょう。Cypressが公式で提供しているサンプルアプリ(https://example.cypress.io/todo)を対象にします。
テストファイルの作成
// cypress/e2e/todo.cy.js
describe('Todoアプリ', () => {
beforeEach(() => {
// 各テストの前にTodoアプリを開く
cy.visit('https://example.cypress.io/todo')
})
it('デフォルトのTodoが2つ表示される', () => {
// Todoリストの項目数を確認
cy.get('.todo-list li').should('have.length', 2)
// 各Todoのテキストを確認
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
})
it('新しいTodoを追加できる', () => {
// 入力欄に新しいTodoを入力してEnterキーを押す
cy.get('[data-test="new-todo"]').type('Buy groceries{enter}')
// Todoリストの項目数が3つになったことを確認
cy.get('.todo-list li').should('have.length', 3)
// 追加されたTodoのテキストを確認
cy.get('.todo-list li').last().should('have.text', 'Buy groceries')
})
it('Todoを完了状態にできる', () => {
// 最初のTodoのチェックボックスをクリック
cy.get('.todo-list li').first().find('.toggle').check()
// 完了状態のクラスが付与されたことを確認
cy.get('.todo-list li').first().should('have.class', 'completed')
})
describe('フィルタリング', () => {
beforeEach(() => {
// テスト前にTodoを1つ完了状態にする
cy.get('.todo-list li').first().find('.toggle').check()
})
it('未完了のTodoのみ表示できる', () => {
// "Active"フィルタをクリック
cy.contains('Active').click()
// 未完了のTodoが1つだけ表示される
cy.get('.todo-list li').should('have.length', 1)
cy.get('.todo-list li').first().should('have.text', 'Walk the dog')
})
it('完了済みのTodoのみ表示できる', () => {
// "Completed"フィルタをクリック
cy.contains('Completed').click()
// 完了済みのTodoが1つだけ表示される
cy.get('.todo-list li').should('have.length', 1)
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
})
it('全てのTodoを表示できる', () => {
// "All"フィルタをクリック
cy.contains('All').click()
// 全てのTodoが表示される
cy.get('.todo-list li').should('have.length', 2)
})
})
})
テストの流れを図で理解する
flowchart TB
subgraph Test1["テスト:デフォルト表示"]
T1A["visit() でページを開く"]
T1B["get() でTodoリストを取得"]
T1C["should() で件数を検証"]
end
subgraph Test2["テスト:Todo追加"]
T2A["visit() でページを開く"]
T2B["get() で入力欄を取得"]
T2C["type() でテキスト入力"]
T2D["should() で件数を検証"]
end
subgraph Test3["テスト:Todo完了"]
T3A["visit() でページを開く"]
T3B["get().find() でチェックを取得"]
T3C["check() でチェック"]
T3D["should() で状態を検証"]
end
T1A --> T1B --> T1C
T2A --> T2B --> T2C --> T2D
T3A --> T3B --> T3C --> T3D
style Test1 fill:#3b82f6,color:#fff
style Test2 fill:#22c55e,color:#fff
style Test3 fill:#8b5cf6,color:#fff
テストの成功と失敗の読み方
成功時の出力
$ npx cypress run --spec "cypress/e2e/todo.cy.js"
Todoアプリ
✓ デフォルトのTodoが2つ表示される (1250ms)
✓ 新しいTodoを追加できる (980ms)
✓ Todoを完了状態にできる (870ms)
フィルタリング
✓ 未完了のTodoのみ表示できる (920ms)
✓ 完了済みのTodoのみ表示できる (890ms)
✓ 全てのTodoを表示できる (850ms)
6 passing (5.8s)
失敗時の出力
テストが失敗すると、Cypressは詳細なエラー情報を表示します。
1) Todoアプリ
新しいTodoを追加できる:
AssertionError: expected 2 to equal 3
+ expected - actual
-2
+3
at Context.eval (cypress/e2e/todo.cy.js:20:44)
失敗時のデバッグポイント
| 情報 | 説明 |
|---|---|
| テスト名 | どのテストが失敗したか |
| エラーメッセージ | 何が期待と違ったか |
| ファイル名と行番号 | コードのどこで失敗したか |
| スクリーンショット | 失敗時のブラウザ画面(cypress/screenshots/ に保存) |
| 動画 | テスト実行の録画(cypress/videos/ に保存、設定時) |
コマンドチェーンの理解
Cypressのコマンドはチェーンで連結されます。これはjQueryに似たパターンです。
cy.get('[data-testid="todo-form"]') // 親要素を取得
.find('input') // 子要素を検索
.type('新しいTodo') // テキスト入力
.should('have.value', '新しいTodo') // 値を検証
親コマンドと子コマンド
flowchart LR
subgraph Parent["親コマンド(起点)"]
P1["cy.visit()"]
P2["cy.get()"]
P3["cy.contains()"]
end
subgraph Child["子コマンド(チェーン)"]
C1[".find()"]
C2[".click()"]
C3[".type()"]
C4[".should()"]
end
P2 --> C1
C1 --> C3
C3 --> C4
style Parent fill:#3b82f6,color:#fff
style Child fill:#22c55e,color:#fff
| 種類 | 説明 | 例 |
|---|---|---|
| 親コマンド | チェーンの起点。cy. で始まる |
cy.get(), cy.visit(), cy.contains() |
| 子コマンド | 前のコマンドの結果に対して実行 | .find(), .click(), .type(), .should() |
| デュアルコマンド | 親としても子としても使える | cy.contains() / .contains() |
まとめ
| 概念 | 説明 |
|---|---|
| describe / it | テストスイートとテストケースを定義するブロック |
| beforeEach | 各テストの前に実行されるセットアップ処理 |
| cy.visit() | 指定したURLのページを開く |
| cy.get() | CSSセレクタでDOM要素を取得する |
| cy.contains() | テキスト内容で要素を検索する |
| .should() | 要素の状態を検証するアサーション |
| cypress open | GUIモードでテストを実行・デバッグ |
| cypress run | ヘッドレスモードでテストを実行(CI/CD向け) |
重要ポイント
beforeEachで各テストの事前条件を整え、テスト間の独立性を保つ- セレクタには
data-testidやdata-cyを使い、テストの安定性を高める .should()の自動リトライ機能により、非同期な要素の変化にも対応できる
練習問題
問題1: 基本
以下の操作をCypressのコードで書いてください。
http://localhost:3000を開くh1タグのテキストが「Welcome」であることを確認するdata-testid="login-btn"のボタンをクリックする
問題2: 応用
以下のテストを describe と it を使って構造化してください。
- ログインフォームにメールアドレスとパスワードを入力する
- 送信ボタンをクリックする
- ダッシュボードページに遷移したことを確認する
- ウェルカムメッセージが表示されることを確認する
チャレンジ問題
Cypressの公式サンプルアプリ(https://example.cypress.io/todo)に対して、以下のテストを追加してください。
- Todoを3つ追加する
- 2番目のTodoを完了状態にする
- "Active"フィルタで未完了のTodoが4つ表示されることを確認する
- "Completed"フィルタで完了済みのTodoが1つ表示されることを確認する
参考リンク
次回予告: Day 3では「要素の操作と検索」について学びます。より高度なセレクタの使い方やDOM操作コマンドを習得しましょう。