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での補完を有効にする宣言ファイル |
重要ポイント
- 繰り返す操作はカスタムコマンドにして再利用性を高める
- ログイン処理には
cy.session()を活用してテスト速度を改善する - 機密情報は
cypress.env.jsonや環境変数で管理し、Gitにコミットしない - カスタムコマンドとユーティリティ関数を使い分け、コードを整理する
練習問題
問題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では「フィクスチャとデータ駆動テスト」について学びます。外部データファイルを活用して、効率的にテストケースを管理する方法を習得しましょう。