跳至主要内容

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列表:

FixtureTypeDescription
pagePageIsolated page for this test run.
contextBrowserContextIsolated context for this test run. The page fixture belongs to this context as well. Learn how to configure context.
browserBrowserBrowsers are shared across tests to optimize resources. Learn how to configure browser.
browserNamestringThe name of the browser currently running the test. Either chromium, firefox or webkit.
requestAPIRequestContext此测试运行的独立APIRequestContext实例。

不使用fixtures

以下是传统测试风格与基于fixture的测试风格在典型测试环境设置上的区别。

TodoPage 是一个帮助与网页应用中"待办事项列表"页面交互的类,遵循 Page Object Model 模式。它在内部使用 Playwright 的 page

Click to expand the code for the TodoPage
待办事项页面.ts
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();
}
}
}
todo.spec.ts
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
待办事项页面.ts
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();
}
}
}
example.spec.ts
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 todoPagesettingsPage

Click to expand the code for the TodoPage and SettingsPage
待办事项页面.ts
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类似:

settings-page.ts
import type { Page } from '@playwright/test';

export class SettingsPage {
constructor(public readonly page: Page) {
}

async switchToDarkMode() {
// ...
}
}

我的测试.ts
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';
note

自定义夹具名称应以字母或下划线开头,且只能包含字母、数字和下划线。

使用夹具

只需在测试函数参数中提到fixture,测试运行器就会处理它。fixture也可用于hooks和其他fixture中。如果您使用TypeScript,fixture将具有正确的类型。

下面我们使用上面定义的 todoPagesettingsPage 夹具。

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()在测试文件中进行本地配置。

example.spec.ts

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'}以便测试运行器为每个工作线程设置此夹具。

我的测试.ts
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对象来检索正在运行的测试的元数据。

我的测试.ts
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
待办事项页面.ts
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();
}
}
}
我的测试.ts
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选项。

playwright.config.ts
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 beforeAll section:
    • browser 设置,因为 autoWorkerFixture 需要它。
    • autoWorkerFixture 设置,因为自动工作器夹具始终在其他任何操作之前设置。
    • beforeAll 运行。
  • first test section:
    • autoTestFixture 设置,因为自动测试夹具总是在测试和beforeEach钩子之前设置好。
    • page 设置,因为它在 beforeEach 钩子中是必需的。
    • beforeEach 运行。
    • first test 运行。
    • afterEach 运行。
    • page 会在测试结束时自动销毁,因为它是一个测试作用域的fixture,应该在测试完成后被销毁。
    • autoTestFixture 拆卸,因为它是一个测试范围的夹具,应该在测试完成后拆除。
  • second test section:
    • autoTestFixture 设置,因为自动测试夹具总是在测试和 beforeEach 钩子之前设置好。
    • page 设置,因为它在 beforeEach 钩子中是必需的。
    • beforeEach 运行。
    • workerFixture 设置,因为它是被 second test 所需的 testFixture 所要求的。
    • testFixture 设置,因为second test需要它。
    • second test 运行。
    • afterEach 运行。
    • testFixture 拆卸,因为它是一个测试范围的夹具,应该在测试完成后拆除。
    • page 会在测试结束时被销毁,因为它是一个测试作用域的fixture,应该在测试完成后被清理。
    • autoTestFixture 拆卸,因为它是一个测试范围的夹具,应该在测试完成后拆除。
  • afterAll and worker teardown section:
    • afterAll 运行。
    • workerFixture 拆卸,因为它是一个 workers-scoped 的 fixture,应该在最后一次性拆卸。
    • autoWorkerFixture 拆卸,因为它是一个 workers-scoped 夹具,应该在最后一次性拆卸。
    • browser 拆卸,因为它是一个 workers-scoped 的 fixture,应该只在最后拆卸一次。

几点观察:

  • pageautoTestFixture 作为测试范围的fixture,会为每个测试进行设置和清理。
  • unusedFixture 从未被设置,因为它没有被任何测试/钩子使用。
  • testFixture 依赖于 workerFixture 并触发其设置。
  • workerFixture 作为工作线程范围的固件,在第二个测试前被延迟设置,但在工作线程关闭时仅拆除一次。
  • autoWorkerFixture 是为 beforeAll 钩子设置的,但 autoTestFixture 不是。

合并来自多个模块的自定义夹具

你可以合并来自多个文件或模块的测试夹具:

fixtures.ts
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);
test.spec.ts
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() 块(如果有的话)。如果您想声明全局在每个测试之前/之后运行的钩子,可以像这样将它们声明为自动夹具:

fixtures.ts
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:

mytest.spec.ts
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' 如下:

fixtures.ts
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:

mytest.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';

test('basic', async ({ }) => {
// ...
});

请注意,fixtures仍会在每个worker process中运行一次,但您不需要在每个文件中重新声明它们。