夾具
簡介
Playwright 測試基於測試夾具的概念。測試夾具用於為每個測試建立環境,為測試提供所需的一切,不多也不少。測試夾具在測試之間是隔離的。使用夾具,您可以根據測試的含義而不是它們的通用設定來對測試進行分組。
內建夾具
您已經在您的第一個測試中使用過測試夾具。
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev.org.tw/');
await expect(page).toHaveTitle(/Playwright/);
});
{ page }
引數告訴 Playwright 測試設定 page
夾具並將其提供給您的測試函式。
以下是您最常使用的預定義夾具列表
夾具 | 類型 | 描述 |
---|---|---|
page | Page | 此測試執行的隔離頁面。 |
context | BrowserContext | 此測試執行的隔離 context。 page 夾具也屬於此 context。 了解如何設定 context。 |
browser | Browser | 瀏覽器在測試之間共用以最佳化資源。 了解如何設定瀏覽器。 |
browserName | 字串 | 目前執行測試的瀏覽器名稱。 chromium 、firefox 或 webkit 其中之一。 |
request | APIRequestContext | 此測試執行的隔離 APIRequestContext 實例。 |
不使用夾具
以下是傳統測試樣式與基於夾具的樣式之間,典型測試環境設定的差異。
TodoPage
是一個類別,有助於與網路應用程式的「待辦事項列表」頁面互動,遵循 頁面物件模型 模式。 它在內部使用 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');
// ...
});
});
使用夾具
夾具比 before/after 鉤子有許多優勢
- 夾具將設定和拆解 封裝 在同一個地方,因此更容易編寫。 因此,如果您有一個 after 鉤子來拆解在 before 鉤子中建立的內容,請考慮將它們變成夾具。
- 夾具在測試檔案之間是 可重複使用 的 - 您可以定義一次,並在所有測試中使用。 這就是 Playwright 的內建
page
夾具的工作方式。 因此,如果您有一個在多個測試中使用的輔助函式,請考慮將其變成夾具。 - 夾具是 隨需應變 的 - 您可以定義任意數量的夾具,而 Playwright 測試只會設定您的測試需要的夾具,而不會設定其他夾具。
- 夾具是 可組合 的 - 它們可以相互依賴以提供複雜的行為。
- 夾具是 彈性 的。 測試可以使用夾具的任何組合來客製化它們需要的精確環境,而不會影響其他測試。
- 夾具簡化了 分組。 您不再需要將測試包裝在設定環境的
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');
// ...
});
建立夾具
若要建立您自己的夾具,請使用 test.extend() 來建立一個新的 test
物件,其中將包含它。
以下我們建立兩個夾具 todoPage
和 settingsPage
,它們遵循 頁面物件模型 模式。
按一下以展開 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';
自訂夾具名稱應以字母或底線開頭,且只能包含字母、數字、底線。
使用夾具
只需在您的測試函式引數中提及夾具,測試執行器就會處理它。 夾具也可用於鉤子和其他夾具中。 如果您使用 TypeScript,夾具將具有正確的類型。
以下我們使用上面定義的 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']);
});
覆寫夾具
除了建立您自己的夾具之外,您還可以覆寫現有的夾具以滿足您的需求。 考慮以下範例,它透過自動導航到某些 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.org.tw' });
夾具也可以在基本夾具完全被其他東西取代的情況下被覆寫。 例如,我們可以覆寫 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 範圍夾具
Playwright 測試使用 worker 處理程序 來執行測試檔案。 與為個別測試執行設定測試夾具類似,為每個 worker 處理程序設定 worker 夾具。 您可以在其中設定服務、執行伺服器等。 Playwright 測試會盡可能重複使用 worker 處理程序來處理許多測試檔案,前提是它們的 worker 夾具匹配,因此環境是相同的。
以下我們將建立一個 account
夾具,該夾具將由同一個 worker 中的所有測試共用,並覆寫 page
夾具,以便為每個測試登入此帳戶。 為了產生唯一的帳戶,我們將使用 workerInfo.workerIndex,它可用於任何測試或夾具。 請注意 worker 夾具的元組狀語法 - 我們必須傳遞 {scope: 'worker'}
,以便測試執行器為每個 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';
自動夾具
即使測試未直接列出自動夾具,也會為每個測試/worker 設定自動夾具。 若要建立自動夾具,請使用元組語法並傳遞 { auto: true }
。
以下是一個範例夾具,它會在測試失敗時自動附加偵錯日誌,以便我們稍後可以在報告器中檢閱日誌。 請注意它如何使用 TestInfo 物件,該物件在每個測試/夾具中都可用,以檢索有關正在執行的測試的中繼資料。
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';
夾具逾時
預設情況下,夾具與測試共用逾時。 但是,對於緩慢的夾具,尤其是 worker 範圍 的夾具,擁有單獨的逾時是很方便的。 這樣您就可以保持整體測試逾時時間較短,並為緩慢的夾具提供更多時間。
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 }) => {
// ...
});
夾具選項
Playwright 測試支援執行可以單獨設定的多個測試專案。 您可以使用「選項」夾具,使您的組態選項具有宣告性和類型檢查。 深入了解參數化測試。
以下我們將在其他範例中的 todoPage
夾具之外,建立一個 defaultItem
選項。 此選項將在組態檔案中設定。 請注意元組語法和 { 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()
呼叫分隔。 設定在測試/鉤子使用夾具之前執行,而拆解在測試/鉤子不再使用夾具時執行。
夾具遵循這些規則來判斷執行順序
- 當夾具 A 依賴於夾具 B 時:B 始終在 A 之前設定,並在 A 之後拆解。
- 非自動夾具會延遲執行,僅在測試/鉤子需要它們時執行。
- 測試範圍夾具在每個測試後拆解,而 worker 範圍夾具僅在執行測試的 worker 處理程序關閉時拆解。
考慮以下範例
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 設定和
beforeAll
區段browser
設定,因為autoWorkerFixture
需要它。autoWorkerFixture
設定,因為自動 worker 夾具始終在任何其他夾具之前設定。beforeAll
執行。
first test
區段autoTestFixture
設定,因為自動測試夾具始終在測試和beforeEach
鉤子之前設定。page
設定,因為beforeEach
鉤子中需要它。beforeEach
執行。first test
執行。afterEach
執行。page
拆解,因為它是測試範圍夾具,應在測試完成後拆解。autoTestFixture
拆解,因為它是測試範圍夾具,應在測試完成後拆解。
second test
區段autoTestFixture
設定,因為自動測試夾具始終在測試和beforeEach
鉤子之前設定。page
設定,因為beforeEach
鉤子中需要它。beforeEach
執行。workerFixture
設定,因為testFixture
需要它,而testFixture
是second test
所需要的。testFixture
設定,因為second test
需要它。second test
執行。afterEach
執行。testFixture
拆解,因為它是測試範圍夾具,應在測試完成後拆解。page
拆解,因為它是測試範圍夾具,應在測試完成後拆解。autoTestFixture
拆解,因為它是測試範圍夾具,應在測試完成後拆解。
afterAll
和 worker 拆解區段afterAll
執行。workerFixture
拆解,因為它是 worker 範圍夾具,應在結束時拆解一次。autoWorkerFixture
拆解,因為它是 worker 範圍夾具,應在結束時拆解一次。browser
拆解,因為它是 worker 範圍夾具,應在結束時拆解一次。
一些觀察結果
page
和autoTestFixture
會針對每個測試設定和拆解,作為測試範圍夾具。unusedFixture
永遠不會設定,因為任何測試/鉤子都不會使用它。testFixture
依賴於workerFixture
並觸發其設定。workerFixture
會在第二個測試之前延遲設定,但在 worker 關閉期間拆解一次,作為 worker 範圍夾具。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.
});
盒裝夾具
通常,自訂夾具會在 UI 模式、追蹤檢視器和各種測試報告中報告為單獨的步驟。 它們也會出現在測試執行器的錯誤訊息中。 對於頻繁使用的夾具,這可能意味著很多雜訊。 您可以透過「盒裝」來停止在 UI 中顯示夾具步驟。
import { test as base } from '@playwright/test';
export const test = base.extend({
helperFixture: [async ({}, use, testInfo) => {
// ...
}, { box: true }],
});
這對於非有趣的輔助夾具很有用。 例如,一個 自動 夾具,用於設定一些通用資料,可以安全地從測試報告中隱藏。
自訂夾具標題
您可以為夾具提供自訂標題,而不是通常的夾具名稱,該標題將顯示在測試報告和錯誤訊息中。
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('https://127.0.0.1:8000');
await use();
// This code runs after every test.
console.log('Last URL:', page.url());
}, { auto: true }], // automatically starts for every test.
});
然後在您的所有測試中匯入夾具
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ page }) => {
expect(page).toHaveURL('https://127.0.0.1:8000');
await page.goto('https://playwright.dev.org.tw');
});
新增全域 beforeAll/afterAll 鉤子
test.beforeAll() 和 test.afterAll() 鉤子在同一個檔案和同一個 test.describe() 區塊(如果有的話)中宣告的所有測試之前/之後執行,每個 worker 處理程序一次。 如果您想要宣告在每個檔案中的所有測試之前/之後執行的鉤子,您可以將它們宣告為具有 scope: 'worker'
的自動夾具,如下所示
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.
});
然後在您的所有測試中匯入夾具
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ }) => {
// ...
});
請注意,夾具仍然會每個 worker 處理程序 執行一次,但您不需要在每個檔案中重新宣告它們。