10日で覚えるCypressDay 2: はじめてのテストを書こう

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向け)

重要ポイント

  1. beforeEach で各テストの事前条件を整え、テスト間の独立性を保つ
  2. セレクタには data-testiddata-cy を使い、テストの安定性を高める
  3. .should() の自動リトライ機能により、非同期な要素の変化にも対応できる

練習問題

問題1: 基本

以下の操作をCypressのコードで書いてください。

  • http://localhost:3000 を開く
  • h1 タグのテキストが「Welcome」であることを確認する
  • data-testid="login-btn" のボタンをクリックする

問題2: 応用

以下のテストを describeit を使って構造化してください。

  • ログインフォームにメールアドレスとパスワードを入力する
  • 送信ボタンをクリックする
  • ダッシュボードページに遷移したことを確認する
  • ウェルカムメッセージが表示されることを確認する

チャレンジ問題

Cypressの公式サンプルアプリ(https://example.cypress.io/todo)に対して、以下のテストを追加してください。

  • Todoを3つ追加する
  • 2番目のTodoを完了状態にする
  • "Active"フィルタで未完了のTodoが4つ表示されることを確認する
  • "Completed"フィルタで完了済みのTodoが1つ表示されることを確認する

参考リンク


次回予告: Day 3では「要素の操作と検索」について学びます。より高度なセレクタの使い方やDOM操作コマンドを習得しましょう。