Day 7: フィクスチャとPage Object Model
今日学ぶこと
- Playwrightのビルトインフィクスチャ(page, browser, context, request, browserName)
- フィクスチャによるテスト分離の仕組み
- test.extend() を使ったカスタムフィクスチャの作成
- フィクスチャのスコープ(test vs worker)
- フィクスチャの合成と依存関係
- Page Object Model(POM)の概念と設計
- POMクラスの作成とフィクスチャとの統合
- プロジェクト内でのフィクスチャとPOMの整理方法
- テストコードにおけるDRY vs WET
フィクスチャとは
フィクスチャは、テストの実行に必要な環境やデータを提供する仕組みです。Playwrightでは、テスト関数の引数としてフィクスチャを受け取ることで、ブラウザやページなどのリソースを自動的に管理できます。
flowchart TB
subgraph Fixture["フィクスチャの役割"]
Setup["セットアップ\nリソースの準備"]
Test["テスト実行"]
Teardown["ティアダウン\nリソースの破棄"]
Setup --> Test --> Teardown
end
subgraph Isolation["テスト分離"]
T1["テスト1\n独自のpage"]
T2["テスト2\n独自のpage"]
T3["テスト3\n独自のpage"]
end
style Fixture fill:#3b82f6,color:#fff
style Isolation fill:#22c55e,color:#fff
ビルトインフィクスチャ
Playwrightは、すぐに使えるビルトインフィクスチャを提供しています。
page
最も頻繁に使うフィクスチャです。テストごとに新しいブラウザコンテキストとページが作成されます。
import { test, expect } from '@playwright/test'
test('ページタイトルを確認', async ({ page }) => {
await page.goto('https://example.com')
await expect(page).toHaveTitle('Example Domain')
})
browser
ブラウザインスタンスそのものにアクセスします。複数のコンテキストを手動で作成したい場合に使います。
test('複数コンテキストで操作', async ({ browser }) => {
const context1 = await browser.newContext()
const context2 = await browser.newContext()
const page1 = await context1.newPage()
const page2 = await context2.newPage()
// 異なるセッションで同時操作
await page1.goto('https://example.com')
await page2.goto('https://example.com')
await context1.close()
await context2.close()
})
context
現在のブラウザコンテキストにアクセスします。Cookie やストレージの操作に便利です。
test('Cookieを設定してテスト', async ({ context, page }) => {
await context.addCookies([{
name: 'session',
value: 'abc123',
domain: 'example.com',
path: '/',
}])
await page.goto('https://example.com/dashboard')
await expect(page.locator('.user-name')).toBeVisible()
})
request
ブラウザを使わずにAPIリクエストを送信できます。
test('APIからデータを取得', async ({ request }) => {
const response = await request.get('https://api.example.com/users')
expect(response.ok()).toBeTruthy()
const users = await response.json()
expect(users.length).toBeGreaterThan(0)
})
browserName
現在実行中のブラウザ名を取得できます。ブラウザ固有の処理に使います。
test('ブラウザに応じた処理', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'WebKitでは未対応')
await page.goto('https://example.com')
// Chromium/Firefox固有のテスト
})
ビルトインフィクスチャ一覧
| フィクスチャ | スコープ | 説明 |
|---|---|---|
| page | test | テストごとの分離されたページ |
| browser | worker | 共有ブラウザインスタンス |
| context | test | テストごとのブラウザコンテキスト |
| request | test | APIリクエストコンテキスト |
| browserName | worker | 実行中のブラウザ名 |
フィクスチャによるテスト分離
各テストは独立したフィクスチャを受け取るため、テスト間の干渉が起きません。
// テスト1とテスト2はそれぞれ別のpageを持つ
test('商品を検索', async ({ page }) => {
await page.goto('https://shop.example.com')
await page.fill('#search', 'keyboard')
await page.click('button[type="submit"]')
// このpageの状態はテスト2には影響しない
})
test('カートに追加', async ({ page }) => {
await page.goto('https://shop.example.com/product/1')
await page.click('#add-to-cart')
// テスト1のpageとは完全に独立
})
カスタムフィクスチャの作成
test.extend() を使って、独自のフィクスチャを定義できます。
基本的なカスタムフィクスチャ
// fixtures.ts
import { test as base } from '@playwright/test'
// カスタムフィクスチャの型定義
type MyFixtures = {
todoPage: Page
}
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
// セットアップ: ページに移動してデータを準備
await page.goto('https://demo.playwright.dev/todomvc/')
await page.fill('.new-todo', 'Buy groceries')
await page.press('.new-todo', 'Enter')
await page.fill('.new-todo', 'Clean house')
await page.press('.new-todo', 'Enter')
// テストにフィクスチャを提供
await use(page)
// ティアダウン: クリーンアップ処理
// use() の後に記述した処理はテスト後に実行される
},
})
export { expect } from '@playwright/test'
// tests/todo.spec.ts
import { test, expect } from '../fixtures'
test('TODOが2件表示される', async ({ todoPage }) => {
const items = todoPage.locator('.todo-list li')
await expect(items).toHaveCount(2)
})
test('TODOを完了にする', async ({ todoPage }) => {
await todoPage.locator('.todo-list li').first().locator('.toggle').click()
const completed = todoPage.locator('.todo-list li.completed')
await expect(completed).toHaveCount(1)
})
認証済みユーザーのフィクスチャ
import { test as base, Page } from '@playwright/test'
type AuthFixtures = {
authenticatedPage: Page
}
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
// APIでログイン
await page.goto('/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', 'password123')
await page.click('button[type="submit"]')
await page.waitForURL('/dashboard')
await use(page)
// ログアウト処理
await page.goto('/logout')
},
})
フィクスチャのスコープ
フィクスチャには2つのスコープがあります。
testスコープ(デフォルト)
テストごとにセットアップ・ティアダウンが実行されます。
export const test = base.extend<{ tempDir: string }>({
tempDir: async ({}, use) => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-'))
await use(dir)
await fs.rm(dir, { recursive: true })
},
})
workerスコープ
ワーカープロセスごとに1回だけセットアップされます。コストの高い初期化に適しています。
import { test as base } from '@playwright/test'
type WorkerFixtures = {
apiServer: string
}
export const test = base.extend<{}, WorkerFixtures>({
apiServer: [async ({}, use) => {
// サーバーを起動(ワーカーごとに1回)
const server = await startTestServer()
await use(server.url)
await server.close()
}, { scope: 'worker' }],
})
| スコープ | セットアップ頻度 | 用途 |
|---|---|---|
| test | テストごと | ページ操作、テストデータ |
| worker | ワーカーごと | サーバー起動、DB接続 |
フィクスチャの合成と依存関係
フィクスチャは他のフィクスチャに依存できます。Playwrightが依存関係を自動的に解決します。
import { test as base, Page } from '@playwright/test'
type Fixtures = {
dbConnection: DatabaseConnection
seedData: TestData
adminPage: Page
}
export const test = base.extend<Fixtures>({
// 基盤となるフィクスチャ
dbConnection: async ({}, use) => {
const db = await connectToTestDB()
await use(db)
await db.close()
},
// dbConnectionに依存するフィクスチャ
seedData: async ({ dbConnection }, use) => {
const data = await dbConnection.seed({
users: [{ name: 'Admin', role: 'admin' }],
products: [{ name: 'Widget', price: 100 }],
})
await use(data)
await dbConnection.cleanup()
},
// seedDataとpageに依存するフィクスチャ
adminPage: async ({ page, seedData }, use) => {
await page.goto('/login')
await page.fill('#email', seedData.users[0].email)
await page.fill('#password', 'password')
await page.click('button[type="submit"]')
await use(page)
},
})
flowchart TB
subgraph Dependencies["フィクスチャの依存関係"]
DB["dbConnection"]
Seed["seedData"]
Admin["adminPage"]
Page["page(ビルトイン)"]
DB --> Seed
Seed --> Admin
Page --> Admin
end
style Dependencies fill:#8b5cf6,color:#fff
Page Object Model(POM)
Page Object Modelは、ページのUI操作をクラスにカプセル化するデザインパターンです。テストコードからページの実装詳細を隠蔽し、保守性を高めます。
flowchart LR
subgraph Without["POMなし"]
T1["テスト1\nセレクタ直書き"]
T2["テスト2\nセレクタ直書き"]
T3["テスト3\nセレクタ直書き"]
end
subgraph With["POMあり"]
POM["LoginPage\nセレクタを一元管理"]
TA["テスト1"]
TB["テスト2"]
TC["テスト3"]
POM --> TA
POM --> TB
POM --> TC
end
style Without fill:#ef4444,color:#fff
style With fill:#22c55e,color:#fff
POMクラスの作成
基本的なPOMクラス
// pages/login-page.ts
import { type Page, type Locator, expect } from '@playwright/test'
export class LoginPage {
readonly page: Page
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
readonly errorMessage: Locator
constructor(page: Page) {
this.page = page
this.emailInput = page.locator('#email')
this.passwordInput = page.locator('#password')
this.submitButton = page.locator('button[type="submit"]')
this.errorMessage = page.locator('.error-message')
}
async goto() {
await this.page.goto('/login')
}
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
async expectError(message: string) {
await expect(this.errorMessage).toHaveText(message)
}
}
複数ページのPOM
// pages/dashboard-page.ts
import { type Page, type Locator, expect } from '@playwright/test'
export class DashboardPage {
readonly page: Page
readonly welcomeMessage: Locator
readonly navMenu: Locator
readonly logoutButton: Locator
constructor(page: Page) {
this.page = page
this.welcomeMessage = page.locator('.welcome')
this.navMenu = page.locator('nav')
this.logoutButton = page.locator('#logout')
}
async expectWelcome(name: string) {
await expect(this.welcomeMessage).toContainText(name)
}
async navigateTo(section: string) {
await this.navMenu.getByRole('link', { name: section }).click()
}
async logout() {
await this.logoutButton.click()
}
}
テストでの使用
// tests/login.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from '../pages/login-page'
import { DashboardPage } from '../pages/dashboard-page'
test('正常にログインできる', async ({ page }) => {
const loginPage = new LoginPage(page)
const dashboardPage = new DashboardPage(page)
await loginPage.goto()
await loginPage.login('user@example.com', 'password123')
await dashboardPage.expectWelcome('User')
})
test('無効な認証情報でエラー表示', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('wrong@example.com', 'wrong')
await loginPage.expectError('Invalid credentials')
})
POMとフィクスチャの統合
POMをフィクスチャとして提供することで、テストコードをさらに簡潔にできます。
// fixtures.ts
import { test as base } from '@playwright/test'
import { LoginPage } from './pages/login-page'
import { DashboardPage } from './pages/dashboard-page'
type Pages = {
loginPage: LoginPage
dashboardPage: DashboardPage
}
export const test = base.extend<Pages>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page))
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page))
},
})
export { expect } from '@playwright/test'
// tests/login.spec.ts
import { test, expect } from '../fixtures'
test('正常にログインできる', async ({ loginPage, dashboardPage }) => {
await loginPage.goto()
await loginPage.login('user@example.com', 'password123')
await dashboardPage.expectWelcome('User')
})
プロジェクト構成
フィクスチャとPOMを整理した推奨ディレクトリ構成です。
project/
├── playwright.config.ts
├── fixtures/
│ ├── index.ts # メインのフィクスチャ定義
│ ├── auth.fixtures.ts # 認証関連フィクスチャ
│ └── db.fixtures.ts # DB関連フィクスチャ
├── pages/
│ ├── login-page.ts
│ ├── dashboard-page.ts
│ ├── settings-page.ts
│ └── index.ts # 全POMのre-export
├── tests/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── register.spec.ts
│ ├── dashboard/
│ │ └── dashboard.spec.ts
│ └── settings/
│ └── settings.spec.ts
└── test-data/
└── users.json
フィクスチャの統合例
// fixtures/index.ts
import { mergeTests } from '@playwright/test'
import { test as authTest } from './auth.fixtures'
import { test as dbTest } from './db.fixtures'
export const test = mergeTests(authTest, dbTest)
export { expect } from '@playwright/test'
DRY vs WET in テストコード
テストコードでは「DRY(Don't Repeat Yourself)」を過度に追求すると、かえって可読性が下がることがあります。
DRYが効果的な場面
// Good: POMによるセレクタの一元管理
export class ProductPage {
readonly addToCartButton: Locator
constructor(page: Page) {
// セレクタが変わってもここだけ修正すればよい
this.addToCartButton = page.locator('[data-testid="add-to-cart"]')
}
}
WET(Write Everything Twice)が適切な場面
// Good: テストの意図が明確で、各テストが独立して読める
test('新規ユーザーがアカウントを作成できる', async ({ page }) => {
await page.goto('/register')
await page.fill('#name', 'Alice')
await page.fill('#email', 'alice@example.com')
await page.fill('#password', 'StrongPass123')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/welcome')
})
test('既存メールでの登録はエラーになる', async ({ page }) => {
await page.goto('/register')
await page.fill('#name', 'Bob')
await page.fill('#email', 'existing@example.com')
await page.fill('#password', 'StrongPass123')
await page.click('button[type="submit"]')
await expect(page.locator('.error')).toHaveText('Email already exists')
})
判断基準
| 観点 | DRY(共通化) | WET(重複許容) |
|---|---|---|
| セレクタ管理 | POMで一元管理 | - |
| セットアップ処理 | フィクスチャで共通化 | - |
| テストステップ | - | 各テストで明示的に記述 |
| アサーション | - | テスト固有の検証を記述 |
| ヘルパー関数 | 3回以上繰り返すなら共通化 | 2回までなら重複OK |
テストコードは「実行可能なドキュメント」です。1つのテストを読むだけで、何をテストしているかが分かることが最も重要です。
まとめ
| 概念 | 説明 |
|---|---|
| ビルトインフィクスチャ | page, browser, context, request, browserName |
| test.extend() | カスタムフィクスチャを定義するAPI |
| testスコープ | テストごとにセットアップ・ティアダウン |
| workerスコープ | ワーカーごとに1回だけ初期化 |
| フィクスチャ合成 | フィクスチャ間の依存関係を宣言的に定義 |
| Page Object Model | ページのUI操作をクラスにカプセル化 |
| POM + フィクスチャ | POMをフィクスチャとして提供し、テストを簡潔に |
重要ポイント
- フィクスチャはテスト分離を保証し、セットアップ・ティアダウンを自動化する
test.extend()でカスタムフィクスチャを作成し、テストの前提条件を宣言的に管理する- POMはセレクタと操作を一元管理し、UIの変更に強いテストを実現する
- POMとフィクスチャを組み合わせることで、最も保守性の高いテストコードが書ける
- DRYとWETのバランスを取り、テストの可読性を最優先にする
練習問題
問題1: 基本
page フィクスチャに依存し、指定URLに移動済みの状態を提供するカスタムフィクスチャ homePage を作成してください。
問題2: 応用
ECサイトを想定した以下のPOMクラスを作成してください。
ProductListPage- 商品一覧の操作(検索、フィルタ、商品選択)ProductDetailPage- 商品詳細の操作(カートに追加、数量変更)CartPage- カートの操作(数量変更、削除、合計確認)
チャレンジ問題
上記のPOMクラスをフィクスチャとして統合し、以下のテストシナリオを実装してください:「商品を検索し、カートに追加し、カート内の合計金額を確認する」。workerスコープのフィクスチャでテスト用APIサーバーのURLを管理することも含めてください。
参考リンク
次回予告: Day 8では「デバッグとトレース」について学びます。Playwright Inspectorやトレースビューアを使って、テストの失敗原因を効率的に特定する方法を習得しましょう。