10日で覚えるCypressDay 4: アサーションをマスターする
books.chapter 410日で覚えるCypress

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秒。コマンドごとにカスタマイズ可能

重要ポイント

  1. should() を優先的に使おう - 自動リトライにより非同期UIでも安定したテストが書ける。expect() はコールバック内で複雑なロジックが必要な場合に使う
  2. 否定アサーションに注意 - not.exist は要素がまだ読み込まれていない場合にも通過する。先にポジティブな確認を行ってから否定を確認する
  3. リトライの範囲を理解する - クエリコマンドはリトライされるが、アクションコマンドはリトライされない。この違いを意識してテストを書く

練習問題

問題1: 基本

以下の要素に対して、適切なアサーションを書いてください。

  1. ナビゲーションバーに「ホーム」「ブログ」「問い合わせ」の3つのリンクがある
  2. 「ホーム」リンクが active クラスを持っている
  3. ロゴ画像が表示されていて、alt属性が「サイトロゴ」である

問題2: 応用

should() コールバック形式を使って、商品価格が1,000円以上10,000円以下であることを確認するテストを書いてください。価格表示は「¥3,500」のような形式です。

チャレンジ問題

以下のシナリオをテストするコードを書いてください。

  • ショッピングカートに3つの商品を追加
  • カートアイコンのバッジに「3」と表示されることを確認
  • カートを開いて合計金額が正しいことを確認(各商品の金額を取得して合算)
  • 1つの商品を削除して、バッジが「2」に更新されることを確認
  • should() コールバックと cy.wrap() を活用すること

参考リンク


次回予告: Day 5では「待機とネットワーク制御」について学びます。cy.intercept() を使ったAPIリクエストの監視とモック、cy.wait() による待機戦略を習得しましょう。