10日で覚えるCypressDay 7: カスタムコマンドとユーティリティ

Day 7: カスタムコマンドとユーティリティ

今日学ぶこと

  • Cypress.Commands.add() でカスタムコマンドを作成する方法
  • 親コマンド、子コマンド、デュアルコマンドの違い
  • ログインコマンドの実践的な作成例
  • cy.session() によるセッション管理
  • support/commands.js の構成と整理
  • TypeScript対応と型定義の追加
  • Cypress.env() による環境変数管理

カスタムコマンドとは

テストを書いていると、ログイン処理やAPIリクエストなど、何度も繰り返す操作が出てきます。Cypressのカスタムコマンドを使えば、これらを再利用可能なコマンドとして定義できます。

flowchart TB
    subgraph Before["カスタムコマンド導入前"]
        T1["テスト1\nログイン処理を記述"]
        T2["テスト2\nログイン処理を記述"]
        T3["テスト3\nログイン処理を記述"]
    end
    subgraph After["カスタムコマンド導入後"]
        CMD["cy.login()\nカスタムコマンド"]
        TA["テスト1"]
        TB["テスト2"]
        TC["テスト3"]
        CMD --> TA
        CMD --> TB
        CMD --> TC
    end
    style Before fill:#ef4444,color:#fff
    style After fill:#22c55e,color:#fff
方法 メリット デメリット
コードのコピペ 簡単 メンテナンスが困難
関数として切り出す 再利用可能 cy チェーンに組み込めない
カスタムコマンド チェーン対応、自然な記法 学習コストがやや高い

Cypress.Commands.add() の基本

カスタムコマンドは cypress/support/commands.js(または .ts)に定義します。

基本構文

Cypress.Commands.add(name, options, callbackFn)
  • name: コマンド名(文字列)
  • options: オプション(省略可能)
  • callbackFn: コマンドの実行内容

はじめてのカスタムコマンド

// cypress/support/commands.js

// シンプルなカスタムコマンド
Cypress.Commands.add('getBySel', (selector) => {
  return cy.get(`[data-testid="${selector}"]`)
})

// テストでの使い方
// cy.getBySel('submit-button').click()
// cypress/support/commands.js

// 複数の引数を持つカスタムコマンド
Cypress.Commands.add('fillForm', (name, email, message) => {
  cy.get('#name').type(name)
  cy.get('#email').type(email)
  cy.get('#message').type(message)
})

// テストでの使い方
// cy.fillForm('Taro', 'taro@example.com', 'Hello!')

コマンドの3つのタイプ

Cypressのカスタムコマンドには3つのタイプがあります。

1. 親コマンド(Parent Command)

チェーンの先頭で使うコマンドです。cy. から始まります。

// 親コマンド: cy.login()
Cypress.Commands.add('login', (username, password) => {
  cy.visit('/login')
  cy.get('#username').type(username)
  cy.get('#password').type(password)
  cy.get('button[type="submit"]').click()
  cy.url().should('include', '/dashboard')
})

2. 子コマンド(Child Command)

前のコマンドの結果に対して操作するコマンドです。prevSubject オプションを使います。

// 子コマンド: .shouldBeVisible()
Cypress.Commands.add('shouldBeVisible', { prevSubject: true }, (subject) => {
  cy.wrap(subject).should('be.visible')
})

// 使い方: cy.get('.header').shouldBeVisible()
// 子コマンド: .typeAndValidate()
Cypress.Commands.add('typeAndValidate', { prevSubject: 'element' }, (subject, text) => {
  cy.wrap(subject).clear().type(text)
  cy.wrap(subject).should('have.value', text)
})

// 使い方: cy.get('#email').typeAndValidate('test@example.com')

3. デュアルコマンド(Dual Command)

親としても子としても使えるコマンドです。

// デュアルコマンド: .highlight() / cy.highlight()
Cypress.Commands.add('highlight', { prevSubject: 'optional' }, (subject, color = 'yellow') => {
  if (subject) {
    cy.wrap(subject).then(($el) => {
      $el.css('background-color', color)
    })
  } else {
    cy.get('body').then(($body) => {
      $body.css('background-color', color)
    })
  }
})

// 使い方:
// cy.get('.important').highlight('red')   // 子コマンドとして
// cy.highlight('blue')                    // 親コマンドとして
flowchart LR
    subgraph Parent["親コマンド"]
        P["cy.login()\ncy.getBySel()"]
    end
    subgraph Child["子コマンド"]
        C[".shouldBeVisible()\n.typeAndValidate()"]
    end
    subgraph Dual["デュアルコマンド"]
        D[".highlight()\nprevSubject: optional"]
    end
    P -->|"チェーンの先頭"| C
    style Parent fill:#3b82f6,color:#fff
    style Child fill:#8b5cf6,color:#fff
    style Dual fill:#f59e0b,color:#fff
タイプ prevSubject 使い方
親コマンド なし(デフォルト) cy.commandName()
子コマンド true または 'element' .commandName()
デュアルコマンド 'optional' 両方OK

実践: ログインコマンドの作成

実際のプロジェクトで最もよく使うカスタムコマンドはログイン処理です。

基本的なUIログイン

// cypress/support/commands.js
Cypress.Commands.add('loginByUI', (username, password) => {
  cy.visit('/login')
  cy.get('[data-testid="username"]').type(username)
  cy.get('[data-testid="password"]').type(password)
  cy.get('[data-testid="login-button"]').click()
  cy.get('[data-testid="dashboard"]').should('be.visible')
})

APIを使った高速ログイン

UIを経由するとテストが遅くなるため、APIで直接ログインする方法がおすすめです。

// cypress/support/commands.js
Cypress.Commands.add('loginByAPI', (username, password) => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { username, password },
  }).then((response) => {
    window.localStorage.setItem('authToken', response.body.token)
  })
})

テストでの使い方

describe('Dashboard', () => {
  beforeEach(() => {
    cy.loginByAPI('admin', 'password123')
    cy.visit('/dashboard')
  })

  it('should display welcome message', () => {
    cy.getBySel('welcome-message').should('contain', 'Welcome, admin')
  })
})

cy.session() によるセッション管理

Cypress 12以降では cy.session() を使って、ログインセッションをキャッシュし、テスト間で再利用できます。

Cypress.Commands.add('login', (username, password) => {
  cy.session(
    [username, password],  // session ID (unique key)
    () => {
      // session setup
      cy.visit('/login')
      cy.get('#username').type(username)
      cy.get('#password').type(password)
      cy.get('button[type="submit"]').click()
      cy.url().should('include', '/dashboard')
    },
    {
      validate() {
        // session validation
        cy.request('/api/auth/me').its('status').should('eq', 200)
      },
    }
  )
})
flowchart TB
    START["cy.login() 呼び出し"] --> CHECK{"セッション\nキャッシュあり?"}
    CHECK -->|"なし"| CREATE["セッション作成\n(ログイン実行)"]
    CHECK -->|"あり"| VALIDATE["セッション検証\n(validate関数)"]
    CREATE --> SAVE["セッションを\nキャッシュに保存"]
    SAVE --> RESTORE["セッション復元"]
    VALIDATE -->|"有効"| RESTORE
    VALIDATE -->|"無効"| CREATE
    RESTORE --> DONE["テスト実行"]
    style START fill:#3b82f6,color:#fff
    style CHECK fill:#f59e0b,color:#fff
    style CREATE fill:#8b5cf6,color:#fff
    style RESTORE fill:#22c55e,color:#fff
セッション管理 説明
セッションID ユーザー名とパスワードの組み合わせでユニークに識別
setup関数 セッション未作成時に実行(ログイン処理)
validate関数 キャッシュされたセッションが有効か検証
キャッシュ テストスイート内でセッションを再利用

support/commands.js の構成

プロジェクトが大きくなると、カスタムコマンドの整理が重要になります。

ファイル分割の例

cypress/
├── support/
│   ├── commands.js          # メインのコマンドファイル
│   ├── commands/
│   │   ├── auth.js          # 認証関連コマンド
│   │   ├── navigation.js    # ナビゲーション関連
│   │   └── api.js           # API関連コマンド
│   ├── e2e.js               # E2Eテストのサポート設定
│   └── utils/
│       ├── helpers.js        # 汎用ヘルパー関数
│       └── constants.js      # 定数定義
// cypress/support/commands/auth.js
Cypress.Commands.add('login', (username, password) => {
  cy.session([username, password], () => {
    cy.request('POST', '/api/auth/login', { username, password })
  })
})

Cypress.Commands.add('logout', () => {
  cy.request('POST', '/api/auth/logout')
  cy.clearCookies()
  cy.clearLocalStorage()
})
// cypress/support/commands/navigation.js
Cypress.Commands.add('visitAndWait', (url) => {
  cy.visit(url)
  cy.get('[data-testid="page-loaded"]').should('exist')
})
// cypress/support/commands.js (main entry)
import './commands/auth'
import './commands/navigation'
import './commands/api'

TypeScript対応

TypeScriptプロジェクトでは、カスタムコマンドの型定義を追加することで、IDEの補完が効くようになります。

型定義ファイルの作成

// cypress/support/index.d.ts
declare namespace Cypress {
  interface Chainable {
    /**
     * Custom command to log in via API
     * @param username - the user's username
     * @param password - the user's password
     */
    login(username: string, password: string): Chainable<void>

    /**
     * Custom command to select element by data-testid
     * @param selector - the data-testid value
     */
    getBySel(selector: string): Chainable<JQuery<HTMLElement>>

    /**
     * Custom command to type and validate input
     * @param text - the text to type
     */
    typeAndValidate(text: string): Chainable<JQuery<HTMLElement>>
  }
}

tsconfig.jsonの設定

// cypress/tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom"],
    "types": ["cypress", "node"]
  },
  "include": ["**/*.ts", "support/index.d.ts"]
}

Cypress.env() による環境変数管理

テストで使う設定値(APIのURL、ユーザー認証情報など)は環境変数で管理するのがベストプラクティスです。

cypress.config.js で定義

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    env: {
      apiUrl: 'http://localhost:3000/api',
      adminUser: 'admin',
      adminPassword: 'admin123',
    },
  },
})

環境変数の参照

// テスト内での使用
cy.request({
  method: 'POST',
  url: `${Cypress.env('apiUrl')}/auth/login`,
  body: {
    username: Cypress.env('adminUser'),
    password: Cypress.env('adminPassword'),
  },
})

環境ごとの設定ファイル

// cypress.env.json (gitignore推奨)
{
  "apiUrl": "http://localhost:3000/api",
  "adminUser": "admin",
  "adminPassword": "secret_password"
}
# CLI から環境変数を渡す
npx cypress run --env apiUrl=http://staging.example.com/api,adminUser=test

# 環境変数ファイルを指定
CYPRESS_API_URL=http://staging.example.com/api npx cypress run
設定方法 優先順位 用途
CLI --env 最高 CI/CDでの一時的な上書き
CYPRESS_* 環境変数 CI/CD環境の設定
cypress.env.json ローカル開発のシークレット
cypress.config.js の env デフォルト値

ユーティリティ関数の整理

カスタムコマンドにしなくてもよい処理は、通常のユーティリティ関数として整理します。

// cypress/support/utils/helpers.js

// ランダムなメールアドレスを生成
export function generateEmail() {
  const timestamp = Date.now()
  return `test-${timestamp}@example.com`
}

// 日付をフォーマット
export function formatDate(date) {
  return date.toISOString().split('T')[0]
}

// テストデータを生成
export function createUser(overrides = {}) {
  return {
    name: 'Test User',
    email: generateEmail(),
    role: 'user',
    ...overrides,
  }
}
// テストでの使い方
import { createUser, formatDate } from '../support/utils/helpers'

describe('User Registration', () => {
  it('should register a new user', () => {
    const user = createUser({ role: 'admin' })
    cy.visit('/register')
    cy.get('#name').type(user.name)
    cy.get('#email').type(user.email)
    cy.get('#role').select(user.role)
    cy.get('button[type="submit"]').click()
    cy.contains(`Welcome, ${user.name}`)
  })
})

まとめ

概念 説明
Cypress.Commands.add() カスタムコマンドを定義するAPI
親コマンド cy. から始まるチェーンの起点
子コマンド 前のsubjectに対して操作(prevSubject: true)
デュアルコマンド 親/子どちらでも使える(prevSubject: 'optional')
cy.session() セッションをキャッシュして再利用
Cypress.env() 環境変数による設定管理
型定義 TypeScriptでの補完を有効にする宣言ファイル

重要ポイント

  1. 繰り返す操作はカスタムコマンドにして再利用性を高める
  2. ログイン処理には cy.session() を活用してテスト速度を改善する
  3. 機密情報は cypress.env.json や環境変数で管理し、Gitにコミットしない
  4. カスタムコマンドとユーティリティ関数を使い分け、コードを整理する

練習問題

問題1: 基本

data-cy 属性で要素を取得するカスタムコマンド cy.getByDataCy(value) を作成してください。

問題2: 応用

以下の機能を持つカスタムコマンドを作成してください。

  • cy.login(email, password) - UIでログイン
  • cy.loginByAPI(email, password) - APIでログイン
  • cy.logout() - ログアウト処理

チャレンジ問題

cy.session() を使って、ログインセッションをキャッシュするカスタムコマンドを作成してください。validate関数でセッションの有効性を確認し、無効な場合は再ログインするようにしましょう。TypeScriptの型定義も追加してください。


参考リンク


次回予告: Day 8では「フィクスチャとデータ駆動テスト」について学びます。外部データファイルを活用して、効率的にテストケースを管理する方法を習得しましょう。