測試佈置
簡介
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
佈置並將其提供給您的測試函式。
以下是您最常使用的預定義佈置清單:
佈置 | 類型 | 說明 |
---|---|---|
page | Page | 此測試執行的隔離頁面。 |
context | BrowserContext | 此測試執行的隔離情境。page 佈置也屬於此情境。了解如何組態設定情境。 |
browser | Browser | 瀏覽器在測試之間共享以最佳化資源。了解如何組態設定瀏覽器。 |
browserName | string | 目前執行測試的瀏覽器名稱。可能是 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 Test 只會設定您的測試需要的佈置,而不是其他。
- 佈置是可組合的 - 它們可以相互依賴以提供複雜的行為。
- 佈置是靈活的。測試可以使用任何佈置組合來精確調整其需求的環境,而不會影響其他測試。
- 佈置簡化了分組。您不再需要將測試包裝在設定其環境的
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 is similar:
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']);
});
覆寫佈置
除了建立您自己的佈置之外,您還可以覆寫現有的佈置以符合您的需求。考慮以下範例,它覆寫 page
佈置,自動導覽到 baseURL
:
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' });
Fixtures can also be overridden, causing the base fixture to be completely replaced with something different. For example, we could override the testOptions.storageState fixture to provide our own data.
import { test as base } from '@playwright/test';
export const test = base.extend({
storageState: async ({}, use) => {
const cookie = await getAuthCookie();
await use({ cookies: [cookie] });
},
});
工作程序範圍的佈置
Playwright Test 使用工作程序來執行測試檔案。類似於如何為個別測試執行設定測試佈置,工作程序佈置是為每個工作程序設定的。這就是您可以設定服務、執行伺服器等的地方。Playwright Test 會為盡可能多的測試檔案重複使用工作程序,前提是它們的工作程序佈置匹配,因此環境相同。
以下我們將建立一個 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 物件來檢索有關正在執行的測試的元資料。
import debug from 'debug';
import 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';
佈置逾時
預設情況下,佈置繼承測試的逾時值。但是,對於緩慢的佈置,特別是工作程序範圍的佈置,有一個單獨的逾時很方便。這樣您可以保持整體測試逾時較小,並給緩慢的佈置更多時間。
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 Test 支援執行可以分別組態設定的多個測試專案。您可以使用「選項」佈置使您的組態設定選項具有宣告性和型別安全性。了解更多關於參數化測試。
以下我們將在其他範例中的 todoPage
佈置之外建立一個 defaultItem
選項。此選項將在組態設定檔案中設定。注意元組語法和 { option: true }
引數。
Click to expand the code for the 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';
We can now use the todoPage
fixture as usual, and set the defaultItem
option in the configuration file.
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!' },
},
]
});
Array as an option value
If the value of your option is an array, for example [{ name: 'Alice' }, { name: 'Bob' }]
, you'll need to wrap it into an extra array when providing the value. This is best illustrated with an example.
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,
});
Reset an option
You can reset an option to the value defined in the config file by setting it to undefined
. Consider the following config that sets a baseURL
:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://playwright.dev',
},
});
You can now configure baseURL
for a file, and also opt-out for a single test.
import { test } from '@playwright/test';
// Configure baseURL for this file.
test.use({ baseURL: 'https://playwright.dev/docs/intro' });
test('check intro contents', async ({ page }) => {
// This test will use "https://playwright.dev/docs/intro" base url as defined above.
});
test.describe(() => {
// Reset the value to a config-defined one.
test.use({ baseURL: undefined });
test('can navigate to intro from the home page', async ({ page }) => {
// This test will use "https://playwright.dev" base url as defined in the config.
});
});
If you would like to completely reset the value to undefined
, use a long-form fixture notation.
import { test } from '@playwright/test';
// Completely unset baseURL for this file.
test.use({
baseURL: [async ({}, use) => use(undefined), { scope: 'test' }],
});
test('no base url', async ({ page }) => {
// This test will not have a base url.
});
執行順序
每個佈置在佈置中的 await use()
呼叫之前和之後都有設定和拆除階段。設定在需要它的測試/掛勾執行之前執行,拆除在佈置不再被測試/掛勾使用時執行。
佈置遵循以下規則來決定執行順序:
- 當佈置 A 依賴於佈置 B 時:B 總是在 A 之前設定並在 A 之後拆除。
- 非自動佈置是延遲執行的,只有當測試/掛勾需要它們時才執行。
- 測試範圍的佈置在每個測試後拆除,而工作程序範圍的佈置只有在執行測試的工作程序拆除時才拆除。
考慮以下範例:
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 () => { /* ... */ });
Normally, if all tests pass and no errors are thrown, the order of execution is as following.
- worker setup and
beforeAll
section:browser
setup because it is required byautoWorkerFixture
.autoWorkerFixture
setup because automatic worker fixtures are always set up before anything else.beforeAll
runs.
first test
section:autoTestFixture
setup because automatic test fixtures are always set up before test andbeforeEach
hooks.page
setup because it is required inbeforeEach
hook.beforeEach
runs.first test
runs.afterEach
runs.page
teardown because it is a test-scoped fixture and should be torn down after the test finishes.autoTestFixture
teardown because it is a test-scoped fixture and should be torn down after the test finishes.
second test
section:autoTestFixture
setup because automatic test fixtures are always set up before test andbeforeEach
hooks.page
setup because it is required inbeforeEach
hook.beforeEach
runs.workerFixture
setup because it is required bytestFixture
that is required by thesecond test
.testFixture
setup because it is required by thesecond test
.second test
runs.afterEach
runs.testFixture
teardown because it is a test-scoped fixture and should be torn down after the test finishes.page
teardown because it is a test-scoped fixture and should be torn down after the test finishes.autoTestFixture
teardown because it is a test-scoped fixture and should be torn down after the test finishes.
afterAll
and worker teardown section:afterAll
runs.workerFixture
teardown because it is a workers-scoped fixture and should be torn down once at the end.autoWorkerFixture
teardown because it is a workers-scoped fixture and should be torn down once at the end.browser
teardown because it is a workers-scoped fixture and should be torn down once at the end.
A few observations:
page
andautoTestFixture
are set up and torn down for each test, as test-scoped fixtures.unusedFixture
is never set up because it is not used by any tests/hooks.testFixture
depends onworkerFixture
and triggers its setup.workerFixture
is lazily set up before the second test, but torn down once during worker shutdown, as a worker-scoped fixture.autoWorkerFixture
is set up forbeforeAll
hook, butautoTestFixture
is not.
結合來自多個模組的自訂佈置
您可以合併來自多個檔案或模組的測試佈置:
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 }],
});
This is useful for non-interesting helper fixtures. For example, an automatic fixture that sets up some common data can be safely hidden from a test report.
自訂佈置標題
您可以為佈置提供自訂標題,而不是使用常見的佈置名稱,該標題將顯示在測試報告和錯誤訊息中。
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.
});
And then import the fixtures in all your tests:
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() 區塊(如果有的話)中宣告的所有測試之前/之後執行,每個工作程序一次。如果您想宣告在每個檔案中所有測試之前/之後執行的掛勾,您可以將它們宣告為具有 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.
});
And then import the fixtures in all your tests:
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ }) => {
// ...
});
Note that the fixtures will still run once per worker process, but you don't need to redeclare them in every file.