Day 4: アサーションをマスターする
今日学ぶこと
- 暗黙的アサーション(should)と明示的アサーション(expect)の違い
- よく使うアサーション一覧
- チェーンアサーション(and)の活用
- should() コールバック形式の使い方
- 否定アサーション
- cy.wrap() とカスタムアサーション
- タイムアウトとリトライメカニズム
アサーションとは
アサーション(assertion)とは、「この要素はこうあるべきだ」というテストの期待値を宣言することです。テストの成否を判定する最も重要な部分です。
Cypressには2種類のアサーション方法があります。
flowchart TB
subgraph Assertions["アサーションの種類"]
IMPLICIT["暗黙的アサーション\nshould() / and()"]
EXPLICIT["明示的アサーション\nexpect()"]
end
IMPLICIT -->|"チェーン可能\n自動リトライ"| RESULT1["推奨"]
EXPLICIT -->|"コールバック内で使用\n複雑なロジック"| RESULT2["必要な場合に使用"]
style IMPLICIT fill:#22c55e,color:#fff
style EXPLICIT fill:#f59e0b,color:#fff
style RESULT1 fill:#22c55e,color:#fff
style RESULT2 fill:#f59e0b,color:#fff
暗黙的アサーション: should()
should() はCypressで最もよく使うアサーション方法です。コマンドチェーンに直接つなげて使います。
// 要素が表示されていることを確認
cy.get('[data-cy="title"]').should('be.visible')
// テキスト内容を確認
cy.get('[data-cy="message"]').should('have.text', 'こんにちは')
// 値を確認
cy.get('[data-cy="email"]').should('have.value', 'user@example.com')
// CSSクラスを確認
cy.get('[data-cy="alert"]').should('have.class', 'alert-danger')
// 要素数を確認
cy.get('li').should('have.length', 5)
should() の特徴
should() は自動リトライします。アサーションが成功するまで、デフォルトで4秒間繰り返し実行されます。これにより、非同期で表示される要素も確実にテストできます。
// ボタンをクリック後、メッセージが表示されるまで自動で待機
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="success"]').should('be.visible') // 最大4秒待機
よく使うアサーション一覧
存在と表示
| アサーション | 説明 | 例 |
|---|---|---|
| exist | DOMに存在する | should('exist') |
| not.exist | DOMに存在しない | should('not.exist') |
| be.visible | 表示されている | should('be.visible') |
| not.be.visible | 表示されていない | should('not.be.visible') |
| be.hidden | 非表示 | should('be.hidden') |
// 要素がDOMに存在するが非表示
cy.get('[data-cy="modal"]').should('exist')
cy.get('[data-cy="modal"]').should('not.be.visible')
// 要素がDOMに存在しない
cy.get('[data-cy="deleted-item"]').should('not.exist')
exist vs be.visible:
existはDOMに存在するかどうか、be.visibleはユーザーに見えているかどうかを判定します。非表示(display: none)の要素はexistだがvisibleではありません。
テキストと内容
| アサーション | 説明 | 例 |
|---|---|---|
| have.text | テキストが完全一致 | should('have.text', 'Hello') |
| contain | テキストを含む | should('contain', 'Hello') |
| include.text | テキストを含む(別表記) | should('include.text', 'Hello') |
| be.empty | 内容が空 | should('be.empty') |
// 完全一致
cy.get('h1').should('have.text', 'Cypressへようこそ')
// 部分一致
cy.get('.description').should('contain', 'テスト')
cy.get('.description').should('include.text', 'テスト')
// テキストが空でないことを確認
cy.get('[data-cy="result"]').should('not.be.empty')
属性と値
| アサーション | 説明 | 例 |
|---|---|---|
| have.value | input の値 | should('have.value', 'text') |
| have.attr | 属性を持つ | should('have.attr', 'href', '/home') |
| have.class | クラスを持つ | should('have.class', 'active') |
| have.id | IDを持つ | should('have.id', 'main') |
| have.css | CSSプロパティ | should('have.css', 'color', 'red') |
// input の値
cy.get('[data-cy="name"]').should('have.value', '田中太郎')
// 属性の確認
cy.get('a.logo').should('have.attr', 'href', '/')
cy.get('input').should('have.attr', 'placeholder', '検索...')
// CSSクラスの確認
cy.get('[data-cy="tab"]').should('have.class', 'active')
cy.get('[data-cy="btn"]').should('not.have.class', 'disabled')
状態
| アサーション | 説明 | 例 |
|---|---|---|
| be.enabled | 有効状態 | should('be.enabled') |
| be.disabled | 無効状態 | should('be.disabled') |
| be.checked | チェック済み | should('be.checked') |
| not.be.checked | 未チェック | should('not.be.checked') |
| be.selected | 選択済み | should('be.selected') |
| be.focused | フォーカス | should('be.focused') |
// ボタンの状態
cy.get('[data-cy="submit"]').should('be.enabled')
cy.get('[data-cy="submit"]').should('be.disabled')
// チェックボックスの状態
cy.get('[data-cy="agree"]').check()
cy.get('[data-cy="agree"]').should('be.checked')
チェーンアサーション: and()
and() は should() のエイリアスで、複数のアサーションを連続して記述できます。可読性が向上します。
// should() + and() で複数のアサーションをチェーン
cy.get('[data-cy="alert"]')
.should('be.visible')
.and('have.class', 'alert-success')
.and('contain', '保存しました')
// リンクの確認
cy.get('[data-cy="home-link"]')
.should('have.attr', 'href', '/')
.and('have.text', 'ホーム')
.and('be.visible')
// input の状態確認
cy.get('[data-cy="email"]')
.should('have.value', 'user@example.com')
.and('have.attr', 'type', 'email')
.and('be.enabled')
flowchart LR
GET["cy.get()"] --> SHOULD["should()\n1つ目の確認"]
SHOULD --> AND1["and()\n2つ目の確認"]
AND1 --> AND2["and()\n3つ目の確認"]
style GET fill:#3b82f6,color:#fff
style SHOULD fill:#22c55e,color:#fff
style AND1 fill:#22c55e,color:#fff
style AND2 fill:#22c55e,color:#fff
should() コールバック形式
should() にコールバック関数を渡すと、より複雑なアサーションを記述できます。コールバック内では expect() を使った明示的アサーションが使えます。
// コールバック形式: 要素のテキストを変数として使う
cy.get('[data-cy="price"]').should(($el) => {
const text = $el.text()
const price = parseInt(text.replace(/[^0-9]/g, ''))
expect(price).to.be.greaterThan(0)
expect(price).to.be.lessThan(100000)
})
// 要素の属性を複合的にチェック
cy.get('[data-cy="progress"]').should(($el) => {
const width = $el.css('width')
const widthNum = parseFloat(width)
expect(widthNum).to.be.greaterThan(0)
})
// 複数の条件を組み合わせる
cy.get('[data-cy="item-list"] li').should(($items) => {
expect($items).to.have.length.greaterThan(0)
expect($items).to.have.length.lessThan(20)
expect($items.first()).to.contain.text('Item')
})
コールバック形式の注意点
// コールバック内では Cypress コマンドは使えない
cy.get('[data-cy="list"]').should(($list) => {
// OK: jQuery / expect を使ったアサーション
expect($list).to.have.length(1)
// NG: Cypress コマンドは使えない
// cy.get('.item') // これはエラーになる
})
コールバックも自動リトライの対象です。コールバック内のアサーションが失敗すると、should() 全体が再実行されます。
明示的アサーション: expect()
expect() はBDDスタイルのアサーションで、Chaiライブラリに基づいています。should() コールバック内や then() 内で使用します。
// then() 内で明示的アサーション
cy.get('[data-cy="count"]').then(($el) => {
const count = parseInt($el.text())
expect(count).to.equal(10)
expect(count).to.be.above(5)
expect(count).to.be.below(20)
})
// 文字列のアサーション
cy.url().then((url) => {
expect(url).to.include('/dashboard')
expect(url).to.match(/\/dashboard\/?$/)
})
// 配列のアサーション
cy.get('li').then(($items) => {
const texts = [...$items].map(el => el.textContent)
expect(texts).to.include('Cypress')
expect(texts).to.have.length(5)
})
should() と then() の違い
| 特性 | should() | then() |
|---|---|---|
| リトライ | 自動リトライする | リトライしない |
| 戻り値 | 元のサブジェクトを維持 | 新しいサブジェクトを返せる |
| 用途 | アサーション | 値を取得して処理 |
// should: リトライあり、サブジェクト維持
cy.get('[data-cy="btn"]')
.should('be.visible') // リトライ対象
.click() // should の後も同じ要素
// then: リトライなし
cy.get('[data-cy="count"]').then(($el) => {
// ここは1回だけ実行される
const num = parseInt($el.text())
cy.log(`Count is: ${num}`)
})
否定アサーション
not を使うことで、条件の否定を表現できます。
// 要素が存在しないことを確認
cy.get('[data-cy="error"]').should('not.exist')
// 要素が非表示であることを確認
cy.get('[data-cy="modal"]').should('not.be.visible')
// クラスを持たないことを確認
cy.get('[data-cy="btn"]').should('not.have.class', 'disabled')
// テキストを含まないことを確認
cy.get('[data-cy="status"]').should('not.contain', 'エラー')
// チェックされていないことを確認
cy.get('[data-cy="option"]').should('not.be.checked')
// 値が空であることを確認
cy.get('[data-cy="input"]').should('have.value', '')
cy.get('[data-cy="input"]').should('not.have.value', 'old text')
否定アサーションの注意点
flowchart TB
subgraph Caution["否定アサーションの落とし穴"]
Q["要素がまだ\n読み込まれていない?"]
Q -->|"not.exist で確認"| PASS["テスト通過\n(偶然)"]
Q -->|"少し後に要素が出現"| FAIL["本当は存在するのに\n見逃してしまう"]
end
style Caution fill:#ef4444,color:#fff
style FAIL fill:#ef4444,color:#fff
// 危険: 要素が非同期で表示される場合
// 要素がまだロードされていないだけで通過してしまう
cy.get('[data-cy="error"]').should('not.exist')
// 安全: まず操作を行い、十分な時間を確保してから確認
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="success"]').should('be.visible') // まず成功を確認
cy.get('[data-cy="error"]').should('not.exist') // その後でエラーがないことを確認
cy.wrap() とカスタムアサーション
cy.wrap() はJavaScriptの値やjQueryオブジェクトをCypressコマンドチェーンに変換します。
// 値をラップしてアサーション
cy.wrap(42).should('equal', 42)
cy.wrap('Hello Cypress').should('include', 'Cypress')
cy.wrap([1, 2, 3]).should('have.length', 3)
// オブジェクトのプロパティをテスト
cy.wrap({ name: 'Cypress', version: '13' })
.should('have.property', 'name', 'Cypress')
// 非同期の値をラップ
cy.get('[data-cy="price"]').then(($el) => {
const price = parseInt($el.text().replace('¥', ''))
cy.wrap(price).should('be.greaterThan', 0)
})
its() で深いプロパティにアクセス
// オブジェクトのプロパティに直接アサーション
cy.wrap({ user: { name: 'Taro', age: 25 } })
.its('user.name')
.should('equal', 'Taro')
// 配列の長さ
cy.get('li').its('length').should('be.greaterThan', 3)
// レスポンスのプロパティ
cy.request('/api/users')
.its('status')
.should('equal', 200)
cy.request('/api/users')
.its('body')
.should('have.length', 10)
タイムアウトとリトライメカニズム
Cypressのアサーションは自動リトライされます。これはCypressの最も重要な特徴の一つです。
flowchart TB
CMD["コマンド実行\n(cy.get)"] --> ASSERT["アサーション\n(should)"]
ASSERT -->|"成功"| PASS["テスト通過"]
ASSERT -->|"失敗"| CHECK["タイムアウト\nチェック"]
CHECK -->|"時間内"| CMD
CHECK -->|"タイムアウト"| FAIL["テスト失敗"]
style CMD fill:#3b82f6,color:#fff
style ASSERT fill:#f59e0b,color:#fff
style PASS fill:#22c55e,color:#fff
style FAIL fill:#ef4444,color:#fff
デフォルトのタイムアウト
| 設定 | デフォルト値 | 説明 |
|---|---|---|
| defaultCommandTimeout | 4000ms | cy.get() 等の一般コマンド |
| pageLoadTimeout | 60000ms | ページ読み込み |
| requestTimeout | 5000ms | cy.request() |
| responseTimeout | 30000ms | レスポンス待機 |
タイムアウトのカスタマイズ
// コマンドごとにタイムアウトを指定
cy.get('[data-cy="result"]', { timeout: 10000 })
.should('be.visible')
// cypress.config.js でグローバルに設定
// module.exports = defineConfig({
// e2e: {
// defaultCommandTimeout: 8000,
// }
// })
リトライの仕組み
// このチェーン全体がリトライされる
cy.get('[data-cy="list"]') // 要素を取得(リトライ対象)
.find('li') // 子要素を検索(リトライ対象)
.should('have.length', 5) // アサーション
// 注意: .click() などのアクションはリトライされない
cy.get('[data-cy="btn"]').click() // クリックは1回のみ実行
cy.get('[data-cy="result"]').should('exist') // これは別途リトライ
リトライ対象:
cy.get(),cy.find(),.should()などのクエリコマンド リトライ対象外:.click(),.type(),.request()などのアクションコマンド
実践: フォームバリデーションのテスト
学んだアサーションを組み合わせて、フォームバリデーションのテストを書きましょう。
describe('お問い合わせフォームのバリデーション', () => {
beforeEach(() => {
cy.visit('/contact')
})
it('空のフォームを送信するとエラーが表示される', () => {
cy.get('[data-cy="submit"]').click()
// 複数のエラーメッセージを確認
cy.get('[data-cy="error-name"]')
.should('be.visible')
.and('have.text', '名前を入力してください')
.and('have.class', 'text-red-500')
cy.get('[data-cy="error-email"]')
.should('be.visible')
.and('contain', 'メールアドレス')
cy.get('[data-cy="error-message"]')
.should('be.visible')
})
it('無効なメールアドレスでエラーが表示される', () => {
cy.get('[data-cy="name"]').type('テスト太郎')
cy.get('[data-cy="email"]').type('invalid-email')
cy.get('[data-cy="message"]').type('テストメッセージ')
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="error-email"]')
.should('be.visible')
.and('contain', '有効なメールアドレス')
// 他のフィールドにはエラーがないことを確認
cy.get('[data-cy="error-name"]').should('not.exist')
cy.get('[data-cy="error-message"]').should('not.exist')
})
it('正しい入力で送信すると成功メッセージが表示される', () => {
cy.get('[data-cy="name"]').type('テスト太郎')
cy.get('[data-cy="email"]').type('test@example.com')
cy.get('[data-cy="message"]').type('これはテストメッセージです。')
cy.get('[data-cy="submit"]').click()
// 成功メッセージの確認
cy.get('[data-cy="success-message"]')
.should('be.visible')
.and('contain', '送信が完了しました')
// フォームがリセットされたことを確認
cy.get('[data-cy="name"]').should('have.value', '')
cy.get('[data-cy="email"]').should('have.value', '')
cy.get('[data-cy="message"]').should('have.value', '')
})
it('文字数制限を超えるとエラーが表示される', () => {
const longText = 'a'.repeat(1001)
cy.get('[data-cy="message"]').type(longText)
cy.get('[data-cy="char-count"]').should(($el) => {
const count = parseInt($el.text())
expect(count).to.be.greaterThan(1000)
})
cy.get('[data-cy="char-count"]')
.should('have.class', 'text-red-500')
})
it('リアルタイムバリデーションが機能する', () => {
// メールフィールドにフォーカスして離れる
cy.get('[data-cy="email"]').focus().blur()
cy.get('[data-cy="error-email"]').should('be.visible')
// 有効なメールアドレスを入力するとエラーが消える
cy.get('[data-cy="email"]').type('valid@example.com')
cy.get('[data-cy="error-email"]').should('not.exist')
})
it('送信ボタンが適切に無効化される', () => {
// 初期状態: 無効
cy.get('[data-cy="submit"]').should('be.disabled')
// 全フィールド入力後: 有効
cy.get('[data-cy="name"]').type('テスト太郎')
cy.get('[data-cy="email"]').type('test@example.com')
cy.get('[data-cy="message"]').type('テストメッセージ')
cy.get('[data-cy="submit"]').should('be.enabled')
// フィールドをクリアすると再び無効
cy.get('[data-cy="name"]').clear()
cy.get('[data-cy="submit"]').should('be.disabled')
})
})
まとめ
| 概念 | 説明 |
|---|---|
| should() | 暗黙的アサーション。自動リトライあり。最もよく使う |
| expect() | 明示的アサーション。コールバック内で使用 |
| and() | should() のエイリアス。チェーンで複数条件を記述 |
| not | 否定アサーション。条件の逆を確認する |
| cy.wrap() | JS値をCypressチェーンに変換する |
| its() | オブジェクトのプロパティに直接アクセスする |
| リトライ | クエリコマンドとアサーションは自動リトライされる |
| タイムアウト | デフォルト4秒。コマンドごとにカスタマイズ可能 |
重要ポイント
- should() を優先的に使おう - 自動リトライにより非同期UIでも安定したテストが書ける。expect() はコールバック内で複雑なロジックが必要な場合に使う
- 否定アサーションに注意 -
not.existは要素がまだ読み込まれていない場合にも通過する。先にポジティブな確認を行ってから否定を確認する - リトライの範囲を理解する - クエリコマンドはリトライされるが、アクションコマンドはリトライされない。この違いを意識してテストを書く
練習問題
問題1: 基本
以下の要素に対して、適切なアサーションを書いてください。
- ナビゲーションバーに「ホーム」「ブログ」「問い合わせ」の3つのリンクがある
- 「ホーム」リンクが
activeクラスを持っている - ロゴ画像が表示されていて、alt属性が「サイトロゴ」である
問題2: 応用
should() コールバック形式を使って、商品価格が1,000円以上10,000円以下であることを確認するテストを書いてください。価格表示は「¥3,500」のような形式です。
チャレンジ問題
以下のシナリオをテストするコードを書いてください。
- ショッピングカートに3つの商品を追加
- カートアイコンのバッジに「3」と表示されることを確認
- カートを開いて合計金額が正しいことを確認(各商品の金額を取得して合算)
- 1つの商品を削除して、バッジが「2」に更新されることを確認
- should() コールバックと cy.wrap() を活用すること
参考リンク
次回予告: Day 5では「待機とネットワーク制御」について学びます。cy.intercept() を使ったAPIリクエストの監視とモック、cy.wait() による待機戦略を習得しましょう。