固定裝置
簡介
Playwright Test 是基於測試夾具的概念。測試夾具用於建立每個測試的環境,提供測試所需的一切,而不多餘。測試夾具在測試之間是隔離的。使用夾具,你可以根據測試的意義來分組,而不是根據它們的共同設定。
內建夾具
你已經在你的第一個測試中使用了測試夾具 。
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
});
{ page }
參數告訴 Playwright Test 設定 page
固定裝置並將其提供給你的測試函式。
以下是您大部分時間可能會使用的預定義裝置列表:
Fixture | Type | Description |
---|---|---|
page | Page | 此測試執行的獨立頁面。 |
context | BrowserContext | 此測試執行的獨立上下文。page 物件也 屬於此上下文。了解如何配置上下文。 |
browser | Browser | 瀏覽器在測試間共享以優化資源。了解如何配置瀏覽器。 |
browserName | string | 當前執行測試的瀏覽器名稱。可以是 chromium 、firefox 或 webkit 。 |
request | APIRequestContext | 此測試執行的獨立 APIRequestContext 實例。 |
Without fixtures
這是典型測試環境設定在傳統測試風格和基於夾具的風格之間的不同之處。
TodoPage
是一個幫助與網頁應用程式的 "todo list" 頁面互動的類別,遵循 Page Object Model 模式。它內部使用 Playwright 的 page
。
點擊展開 TodoPage
的程式碼
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
const { test } = require('@playwright/test');
const { TodoPage } = require('./todo-page');
test.describe('todo tests', () => {
let todoPage;
test.beforeEach(async ({ page }) => {
todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
});
test.afterEach(async () => {
await todoPage.removeAll();
});
test('should add an item', async () => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async () => {
await todoPage.remove('item1');
// ...
});
});
使用 fixtures
裝置比 before/after 鉤子有許多優點:
- Fixtures 封裝 設定和拆卸在同一個地方,因此更容易編寫。
- Fixtures 是 可重用 的,可以在測試文件之間重用 - 你可以定義一次,然後在所有測試中使用。這就是 Playwright 內建的
page
fixture 的工作方式。 - Fixtures 是 按需 的 - 你可以定義任意多的 fixtures,Playwright Test 只會設定測試所需的那些,其他的不會設定。
- Fixtures 是 可組合 的 - 它們可以相互依賴以提供複雜的行為。
- Fixtures 是 靈活 的。測試可以使用任意組合的 fixtures 來定制所需的精確環境,而不影響其他測試。
- Fixtures 簡化了 分組。你不再需要在
describe
中包裝測試來設定環境,可以自由地按測試的意義來分組。
點擊展開 TodoPage
的程式碼
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// Extend basic test by providing a "todoPage" fixture.
const test = base.extend<{ todoPage: TodoPage }>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
await use(todoPage);
await todoPage.removeAll();
},
});
test('should add an item', async ({ todoPage }) => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async ({ todoPage }) => {
await todoPage.remove('item1');
// ...
});
建立一個 fixture
要建立您自己的夾具,使用 test.extend() 建立一個新的 test
物件來包含它。
以下我們建立兩個 fixtures todoPage
和 settingsPage
,它們遵循 Page Object Model 模式。
點擊展開 TodoPage
和 SettingsPage
的程式碼
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
SettingsPage 類似:
import type { Page } from '@playwright/test';
export class SettingsPage {
constructor(public readonly page: Page) {
}
async switchToDarkMode() {
// ...
}
}
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
import { SettingsPage } from './settings-page';
// Declare the types of your fixtures.
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// Extend base test by providing "todoPage" and "settingsPage".
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
// Set up the fixture.
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// Use the fixture value in the test.
await use(todoPage);
// Clean up the fixture.
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
export { expect } from '@playwright/test';
自訂夾具名稱應以字母或底線開頭,且只能包含字母、數字、底線。
使用一個 fixture
只需在測試函式參數中提及 fixture,測試執行器會處理它。Fixtures 也可以在 hooks 和其他 fixtures 中使用。如果你使用 TypeScript,fixtures 會有正確 的類型。
以下我們使用上面定義的 todoPage
和 settingsPage
固定裝置。
import { test, expect } from './my-test';
test.beforeEach(async ({ settingsPage }) => {
await settingsPage.switchToDarkMode();
});
test('basic test', async ({ todoPage, page }) => {
await todoPage.addToDo('something nice');
await expect(page.getByTestId('todo-title')).toContainText(['something nice']);
});
覆蓋 fixtures
除了建立您自己的固定裝置之外,您還可以覆寫現有的固定裝置以符合您的需求。考慮以下範例,它透過自動導航到某個 baseURL
來覆寫 page
固定裝置:
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ baseURL, page }, use) => {
await page.goto(baseURL);
await use(page);
},
});
注意,在此範例中,page
固定裝置能夠依賴其他內建固定裝置,例如 testOptions.baseURL。我們現在可以在配置文件中配置 baseURL
,或者在測試文件中本地配置 test.use()。
test.use({ baseURL: 'https://playwright.dev' });
固定裝置也可以被覆蓋,其中基礎固定裝置被完全替換為不同的東西。例如,我們可以覆蓋 testOptions.storageState 固定裝置來提供我們自己的資料。
import { test as base } from '@playwright/test';
export const test = base.extend({
storageState: async ({}, use) => {
const cookie = await getAuthCookie();
await use({ cookies: [cookie] });
},
});
Worker-scoped fixtures
Playwright Test 使用 worker processes 執行測試檔案。類似於如何為個別測試執行設定測試夾具,worker 夾具是為每個 worker process 設定的。這就是你可以設定服務、執行伺服器等的地方。只要它們的 worker 夾具匹配,並且環境相同,Playwright Test 將會重用 worker process 來執行儘可能多的測試檔案。
接下來我們將建立一個 account
固定裝置,該固定裝置將由同一個工作者中的所有測試共享,並覆蓋 page
固定裝置以便每個測試都能登錄到此帳戶。為了生成唯一的帳戶,我們將使用 workerInfo.workerIndex,它可供任何測試或固定裝置使用。請注意工作者固定裝置的類元組語法 - 我們必須傳遞 {scope: 'worker'}
以便測試執行器為每個工作者設定此固定裝置一次。
import { test as base } from '@playwright/test';
type Account = {
username: string;
password: string;
};
// Note that we pass worker fixture types as a second template parameter.
export const test = base.extend<{}, { account: Account }>({
account: [async ({ browser }, use, workerInfo) => {
// Unique username.
const username = 'user' + workerInfo.workerIndex;
const password = 'verysecure';
// Create the account with Playwright.
const page = await browser.newPage();
await page.goto('/signup');
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByText('Sign up').click();
// Make sure everything is ok.
await expect(page.getByTestId('result')).toHaveText('Success');
// Do not forget to cleanup.
await page.close();
// Use the account value.
await use({ username, password });
}, { scope: 'worker' }],
page: async ({ page, account }, use) => {
// Sign in with our account.
const { username, password } = account;
await page.goto('/signin');
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByText('Sign in').click();
await expect(page.getByTestId('userinfo')).toHaveText(username);
// Use signed-in page in the test.
await use(page);
},
});
export { expect } from '@playwright/test';
自動夾具
自動夾具會為每個測試/工作者設定,即使測試沒有直接列出它們。要建立自動夾具,請使用元組語法並傳遞 { auto: true }
。
這裡是一個範例裝置,當測試失敗時會自動附加除錯日誌,因此我們可以稍後在報告中查看日誌。請注意它如何使用 TestInfo 物件來檢索有關正在執行測試的 Metadata。
import * as debug from 'debug';
import * as fs from 'fs';
import { test as base } from '@playwright/test';
export const test = base.extend<{ saveLogs: void }>({
saveLogs: [async ({}, use, testInfo) => {
// Collecting logs during the test.
const logs = [];
debug.log = (...args) => logs.push(args.map(String).join(''));
debug.enable('myserver');
await use();
// After the test we can check whether the test passed or failed.
if (testInfo.status !== testInfo.expectedStatus) {
// outputPath() API guarantees a unique file name.
const logFile = testInfo.outputPath('logs.txt');
await fs.promises.writeFile(logFile, logs.join('\n'), 'utf8');
testInfo.attachments.push({ name: 'logs', contentType: 'text/plain', path: logFile });
}
}, { auto: true }],
});
export { expect } from '@playwright/test';
固定裝置超時
根據預設,fixture 與測試共用超時時間。然而,對於速度較慢的 fixtures,特別是 worker-scoped ones,擁有單獨的超時時間會更方便。這樣你可以保持整體測試的超時時間較短,並給予速度較慢的 fixture 更多時間。
import { test as base, expect } from '@playwright/test';
const test = base.extend<{ slowFixture: string }>({
slowFixture: [async ({}, use) => {
// ... perform a slow operation ...
await use('hello');
}, { timeout: 60000 }]
});
test('example test', async ({ slowFixture }) => {
// ...
});
Fixtures-options
Playwright Test 支援執行多個測試專案,這些專案可以分別配置。你可以使用 "option" 固定裝置使你的配置選項聲明式並進行型別檢查。了解更多關於參數化測試。
接下來我們將建立一個 defaultItem
選項,除了其他範例中的 todoPage
固定裝置外。此選項將在配置文件中設定。請注意元組語法和 { option: true }
參數。
點擊展開 TodoPage
的程式碼
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// Declare your options to type-check your configuration.
export type MyOptions = {
defaultItem: string;
};
type MyFixtures = {
todoPage: TodoPage;
};
// Specify both option and fixture types.
export const test = base.extend<MyOptions & MyFixtures>({
// Define an option and provide a default value.
// We can later override it in the config.
defaultItem: ['Something nice', { option: true }],
// Our "todoPage" fixture depends on the option.
todoPage: async ({ page, defaultItem }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo(defaultItem);
await use(todoPage);
await todoPage.removeAll();
},
});
export { expect } from '@playwright/test';
我們現在可以像往常一樣使用 todoPage
固件,並在配置文件中設置 defaultItem
選項。
import { defineConfig } from '@playwright/test';
import type { MyOptions } from './my-test';
export default defineConfig<MyOptions>({
projects: [
{
name: 'shopping',
use: { defaultItem: 'Buy milk' },
},
{
name: 'wellbeing',
use: { defaultItem: 'Exercise!' },
},
]
});
陣列作為選項值
如果你的選項值是一個陣列,例如 [{ name: 'Alice' }, { name: 'Bob' }]
,你需要在提供值時將其包裝成一個額外的陣列。這最好用一個範例來說明。
type Person = { name: string };
const test = base.extend<{ persons: Person[] }>({
// Declare the option, default value is an empty array.
persons: [[], { option: true }],
});
// Option value is an array of persons.
const actualPersons = [{ name: 'Alice' }, { name: 'Bob' }];
test.use({
// CORRECT: Wrap the value into an array and pass the scope.
persons: [actualPersons, { scope: 'test' }],
});
test.use({
// WRONG: passing an array value directly will not work.
persons: actualPersons,
});
執行順序
每個裝置都有一個設定和拆卸階段,這兩個階段由裝置中的 await use()
呼叫分隔。設定在測試/掛鉤使用裝置之前執行,拆卸在測試/掛鉤不再使用裝置時執行。
Fixtures 遵循以下規則來確定執行順序:
- 當 fixture A 依賴 fixture B 時: B 總是在 A 之前設定,並在 A 之後拆除。
- 非自動 fixture 是懶執行的,只有在測試/鉤子需要它們時才執行。
- 測試範圍的 fixture 在每次測試後拆除,而工作者範圍的 fixture 只有在執行測試的工作者程序關閉時才拆除。
考慮以下範例:
import { test as base } from '@playwright/test';
const test = base.extend<{
testFixture: string,
autoTestFixture: string,
unusedFixture: string,
}, {
workerFixture: string,
autoWorkerFixture: string,
}>({
workerFixture: [async ({ browser }) => {
// workerFixture setup...
await use('workerFixture');
// workerFixture teardown...
}, { scope: 'worker' }],
autoWorkerFixture: [async ({ browser }) => {
// autoWorkerFixture setup...
await use('autoWorkerFixture');
// autoWorkerFixture teardown...
}, { scope: 'worker', auto: true }],
testFixture: [async ({ page, workerFixture }) => {
// testFixture setup...
await use('testFixture');
// testFixture teardown...
}, { scope: 'test' }],
autoTestFixture: [async () => {
// autoTestFixture setup...
await use('autoTestFixture');
// autoTestFixture teardown...
}, { scope: 'test', auto: true }],
unusedFixture: [async ({ page }) => {
// unusedFixture setup...
await use('unusedFixture');
// unusedFixture teardown...
}, { scope: 'test' }],
});
test.beforeAll(async () => { /* ... */ });
test.beforeEach(async ({ page }) => { /* ... */ });
test('first test', async ({ page }) => { /* ... */ });
test('second test', async ({ testFixture }) => { /* ... */ });
test.afterEach(async () => { /* ... */ });
test.afterAll(async () => { /* ... */ });
通常,如果所有測試都通過且沒有錯誤拋出,執行順序如下。
- worker setup 和
beforeAll
部分:browser
設定因為它是autoWorkerFixture
所需的。autoWorkerFixture
設定因為自動 worker fixtures 總是在其他任何東西之前設定。beforeAll
執行。
first test
部分:autoTestFixture
設定因為自動 test fixtures 總是在測試和beforeEach
鉤子之前設定。page
設定因為它在beforeEach
鉤子中需要。beforeEach
執行。first test
執行。afterEach
執行。page
拆卸因為它是測試範圍的 fixture,應該在測試完成後拆卸。autoTestFixture
拆卸因為它是測試範圍的 fixture,應該在測試完成後拆卸。
second test
部分:autoTestFixture
設定因為自動 test fixtures 總是在測試和beforeEach
鉤子之前設定。page
設定因為它在beforeEach
鉤子中需要。beforeEach
執行。workerFixture
設定因為它是testFixture
所需的,而testFixture
是second test
所需的。testFixture
設定因為它是second test
所需的。second test
執行。afterEach
執行。testFixture
拆卸因為它是測試範圍的 fixture,應該在測試完成後拆卸。page
拆卸因為它是測試範圍的 fixture,應該在測試完成後拆卸。autoTestFixture
拆卸因為它是測試範圍的 fixture,應該在測試完成後拆卸。
afterAll
和 worker 拆卸部分:afterAll
執行。workerFixture
拆卸因為它是 worker 範圍的 fixture,應該在最後一次拆卸。autoWorkerFixture
拆卸因為它是 worker 範圍的 fixture,應該在最後一次拆卸。browser
拆卸因為它是 worker 範圍的 fixture,應該在最後一次拆卸。
一些觀察:
page
和autoTestFixture
會在每個測試中設定和拆除,作為測試範圍的裝置。unusedFixture
從未被設定,因為它沒有被任何測試/鉤子使用。testFixture
依賴於workerFixture
並觸發其設定。workerFixture
在第二個測試之前懶惰地設定,但在工作者關閉期間被拆除一次,作為工作者範圍的裝置。autoWorkerFixture
為beforeAll
鉤子設定,但autoTestFixture
則不會。
結合來自多個模組的自訂夾具
你可以合併來自多個文件或模組的測試夾具:
import { mergeTests } from '@playwright/test';
import { test as dbTest } from 'database-test-utils';
import { test as a11yTest } from 'a11y-test-utils';
export const test = mergeTests(dbTest, a11yTest);
import { test } from './fixtures';
test('passes', async ({ database, page, a11y }) => {
// use database and a11y fixtures.
});
Box fixtures
通常,自訂的固定裝置會在 UI 模式、Trace Viewer 和各種測試報告中作為單獨的步驟報告。它們也會出現在測試執行器的錯誤訊息中。對於經常使用的固定裝置,這可能意味著大量的噪音。您可以通過“框選”來停止在 UI 中顯示固定裝置的步驟。
import { test as base } from '@playwright/test';
export const test = base.extend({
helperFixture: [async ({}, use, testInfo) => {
// ...
}, { box: true }],
});
這對於不太有趣的輔助裝置很有用。例如,一個automatic 裝置設定了一些常見的資料 ,可以安全地從測試報告中隱藏。
自訂固定裝置標題
取代通常的固定名稱,你可以給固定物件一個自訂標題,這將顯示在測試報告和錯誤訊息中。
import { test as base } from '@playwright/test';
export const test = base.extend({
innerFixture: [async ({}, use, testInfo) => {
// ...
}, { title: 'my fixture' }],
});
添加全域 beforeEach/afterEach 鉤子
test.beforeEach() 和 test.afterEach() 鉤子在同一個文件和同一個 test.describe() 區塊中宣告的每個測試之前/之後執行。如果你想要宣告在每個測試之前/之後全域執行的鉤子,你可以將它們宣告為自動夾具,如下所示:
import { test as base } from '@playwright/test';
export const test = base.extend<{ forEachTest: void }>({
forEachTest: [async ({ page }, use) => {
// This code runs before every test.
await page.goto('http://localhost:8000');
await use();
// This code runs after every test.
console.log('Last URL:', page.url());
}, { auto: true }], // automatically starts for every test.
});
然後在所有測試中匯入這些 fixtures:
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ page }) => {
expect(page).toHaveURL('http://localhost:8000');
await page.goto('https://playwright.dev');
});
添加全域 beforeAll/afterAll 鉤子
test.beforeAll() 和 test.afterAll() 鉤子在同一個檔案和同一個 test.describe() 區塊(如果有的話)中宣告的所有測試之前/之後執行,每個 worker process 執行一次。如果你想宣告在每個檔案中的所有測試之前/之後執行的鉤子,你可以將它們宣告為 scope: 'worker'
的自動 fixtures,如下所示:
import { test as base } from '@playwright/test';
export const test = base.extend<{}, { forEachWorker: void }>({
forEachWorker: [async ({}, use) => {
// This code runs before all the tests in the worker process.
console.log(`Starting test worker ${test.info().workerIndex}`);
await use();
// This code runs after all the tests in the worker process.
console.log(`Stopping test worker ${test.info().workerIndex}`);
}, { scope: 'worker', auto: true }], // automatically starts for every worker.
});
然後在所有測試中匯入這些 fixtures:
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ }) => {
// ...
});
請注意,這些固定裝置仍然會在每個worker process 執行一次,但你不需要在每個檔案中重新宣告它們。