Fixtures
简介
Playwright Test基于测试夹具的概念。测试夹具用于为每个测试建立环境,为测试提供它所需的一切且仅此而已。测试夹具在测试之间是隔离的。通过夹具,您可以根据测试的含义而非共同的设置来分组测试。
内置fixture
您已经在第一个测试中使用了测试夹具。
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列表:
| Fixture | Type | Description |
|---|---|---|
| page | Page | Isolated page for this test run. |
| context | BrowserContext | Isolated context for this test run. The page fixture belongs to this context as well. Learn how to configure context. |
| browser | Browser | Browsers are shared across tests to optimize resources. Learn how to configure browser. |
| browserName | string | The name of the browser currently running the test. Either chromium, firefox or webkit. |
| request | APIRequestContext | 此测试运行的独立APIRequestContext实例。 |
不使用fixtures
以下是传统测试风格与基于fixture的测试风格在典型测试环境设置上的区别。
TodoPage 是一个帮助与网页应用中"待办事项列表"页面交互的类,遵循 Page Object Model 模式。它在内部使用 Playwright 的 page。
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();
}
}
}
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具有以下优势:
- 夹具封装了设置和拆卸操作在同一位置,因此更易于编写。所以如果您有一个after钩子用于拆卸before钩子中创建的内容,可以考虑将它们转换为夹具。
- 夹具在测试文件之间是可复用的 - 您可以定义一次并在所有测试中使用。这就是Playwright内置的
page夹具的工作原理。因此,如果您有一个在多个测试中使用的辅助函数,考虑将其转换为夹具。 - 夹具是按需的 - 你可以定义任意数量的夹具,而Playwright Test只会设置你的测试所需的那些,不会多也不会少。
- Fixtures是可组合的 - 它们可以相互依赖以提供复杂的行为。
- Fixture是灵活的。测试可以使用任意组合的fixture来定制它们需要的精确环境,而不会影响其他测试。
- Fixtures简化了分组。您不再需要将测试包裹在设置环境的
describe块中,可以自由地根据测试的实际意义进行分组。
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';
// 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
要创建自己的fixture,使用test.extend()来创建一个新的test对象,该对象将包含它。
下面我们创建两个遵循Page Object Model模式的fixture todoPage和settingsPage。
Click to expand the code for the TodoPage and 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也可用于hooks和其他fixture中。如果您使用TypeScript,fixture将具有正确的类型。
下面我们使用上面定义的 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
除了创建自己的fixtures外,您还可以覆盖现有的fixtures以满足您的需求。考虑以下示例,它通过自动导航到某个baseURL来覆盖page fixture:
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 fixture能够依赖于其他内置fixture,例如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作用域的fixtures
Playwright Test 使用 worker 进程来运行测试文件。类似于为单个测试运行设置测试夹具的方式,worker 夹具是为每个 worker 进程设置的。您可以在那里设置服务、运行服务器等。只要它们的 worker 夹具匹配且环境相同,Playwright Test 将尽可能多地重用 worker 进程来运行多个测试文件。
下面我们将创建一个account夹具,该夹具将由同一工作线程中的所有测试共享,并重写page夹具以便为每个测试登录此账户。为了生成唯一账户,我们将使用任何测试或夹具都可访问的workerInfo.workerIndex。请注意worker夹具的元组式语法 - 我们必须传递{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';
Fixture超时
默认情况下,fixture与测试共享超时时间。但对于慢速fixture,特别是worker-scoped类型的,单独设置超时会更方便。这样您可以保持整体测试超时较短,同时给予慢速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选项
Playwright Test 支持运行可单独配置的多个测试项目。您可以使用"option" fixtures使您的配置选项具有声明性并进行类型检查。了解更多关于参数化测试的信息。
下面我们将在其他示例中的todoPage fixture之外,创建一个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';
我们现在可以像往常一样使用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,
});
执行顺序
每个fixture都有一个setup和teardown阶段,由fixture中的await use()调用分隔。setup在测试/钩子使用fixture之前执行,而teardown在fixture不再被测试/钩子使用时执行。
Fixture遵循以下规则来确定执行顺序:
- 当夹具A依赖于夹具B时:B总是在A之前设置,并在A之后拆除。
- 非自动固件(fixtures)是惰性执行的,仅在测试/钩子需要它们时才会运行。
- 测试作用域的fixture在每个测试后会被拆除,而worker作用域的fixture仅在执行测试的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 setup and
beforeAllsection:browser设置,因为autoWorkerFixture需要它。autoWorkerFixture设置,因为自动工作器夹具始终在其他任何操作之前设置。beforeAll运行。
first testsection:autoTestFixture设置,因为自动测试夹具总是在测试和beforeEach钩子之前设置好。page设置,因为它在beforeEach钩子中是必需的。beforeEach运行。first test运行。afterEach运行。page会在测试结束时自动销毁,因为它是一个测试作用域的fixture,应该在测试完成后被销毁。autoTestFixture拆卸,因为它是一个测试范围的夹具,应该在测试完成后拆除。
second testsection:autoTestFixture设置,因为自动测试夹具总是在测试和beforeEach钩子之前设置好。page设置,因为它在beforeEach钩子中是必需的。beforeEach运行。workerFixture设置,因为它是被second test所需的testFixture所要求的。testFixture设置,因为second test需要它。second test运行。afterEach运行。testFixture拆卸,因为它是一个测试范围的夹具,应该在测试完成后拆除。page会在测试结束时被销毁,因为它是一个测试作用域的fixture,应该在测试完成后被清理。autoTestFixture拆卸,因为它是一个测试范围的夹具,应该在测试完成后拆除。
afterAlland worker teardown section:afterAll运行。workerFixture拆卸,因为它是一个 workers-scoped 的 fixture,应该在最后一次性拆卸。autoWorkerFixture拆卸,因为它是一个 workers-scoped 夹具,应该在最后一次性拆卸。browser拆卸,因为它是一个 workers-scoped 的 fixture,应该只在最后拆卸一次。
几点观察:
page和autoTestFixture作为测试范围的fixture,会为每个测试进行设置和清理。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 固件
通常情况下,自定义fixture会在UI模式、Trace Viewer和各种测试报告中作为独立步骤显示。它们也会出现在测试运行器的错误信息中。对于频繁使用的fixture,这可能会造成大量干扰信息。您可以通过"boxing"方式来阻止这些fixture步骤在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('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() 块(如果有的话)中,每个工作进程运行一次。如果你想声明在每个文件的所有测试之前/之后运行的钩子,可以将它们声明为自动夹具,使用 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.
});
然后在所有测试中导入fixtures:
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ }) => {
// ...
});
请注意,fixtures仍会在每个worker process中运行一次,但您不需要在每个文件中重新声明它们。