高级:Fixtures
Playwright Test 基于测试 fixtures 的概念。测试 fixtures 用于为每个测试建立环境,为测试提供所需的一切,而不提供其他任何东西。测试 fixtures 在测试之间是隔离的。使用 fixtures,您可以根据测试的含义对测试进行分组,而不是根据它们的通用设置。
内置 Fixtures
您已经在第一个测试中使用了测试 fixtures。
- TypeScript
- JavaScript
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
const title = page.locator('.navbar__inner .navbar__title');
await expect(title).toHaveText('Playwright');
});
const { test, expect } = require('@playwright/test');
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
const title = page.locator('.navbar__inner .navbar__title');
await expect(title).toHaveText('Playwright');
});
{ page } 参数告诉 Playwright Test 设置 page fixture 并将其提供给您的测试函数。
以下是您最有可能经常使用的预定义 fixtures 列表:
| Fixture | 类型 | 描述 |
|---|---|---|
| page | Page | 此测试运行的隔离页面。 |
| context | BrowserContext | 此测试运行的隔离上下文。page fixture 也属于此上下文。了解如何配置上下文。 |
| browser | Browser | 浏览器在测试之间共享以优化资源。了解如何配置浏览器。 |
| browserName | string | 当前运行测试的浏览器名称。chromium、firefox 或 webkit 之一。 |
不使用 Fixtures
以下是传统测试风格和基于 fixture 的测试风格之间典型测试环境设置的差异。
我们假设有一个 TodoPage 类,它遵循 Page Object Model 模式,帮助与 Web 应用程序的"待办事项列表"页面交互。它在内部使用 Playwright 的 page。
// todo.spec.js
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
Fixtures 相对于 before/after 钩子有许多优势:
- Fixtures 封装了设置和拆卸在同一个地方,因此更容易编写。
- Fixtures 在测试文件之间是可重用的 - 您可以定义一次并在所有测试中使用。这就是 Playwright 内置
pagefixture 的工作方式。 - Fixtures 是按需的 - 您可以定义任意数量的 fixtures,Playwright Test 将仅设置测试所需的 fixtures,而不设置其他任何内容。
- Fixtures 是可组合的 - 它们可以相互依赖以提供复杂的行为。
- Fixtures 是灵活的。测试可以使用 fixtures 的任何组合来定制它们需要的精确环境,而不会影响其他测试。
- Fixtures 简化了分组。您不再需要将测试包装在设置环境的
describe中,而是可以根据测试的含义自由分组测试。
- TypeScript
- JavaScript
// example.spec.ts
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// 通过提供 "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');
// ...
});
// todo.spec.js
const base = require('@playwright/test');
const { TodoPage } = require('./todo-page');
// 通过提供 "todoPage" fixture 扩展基本测试。
const test = base.test.extend({
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(fixtures) 创建一个包含它的新 test 对象。
下面我们创建两个遵循 Page Object Model 模式的 fixtures todoPage 和 settingsPage。
- TypeScript
- JavaScript
// my-test.ts
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
import { SettingsPage } from './settings-page';
// 声明 fixtures 的类型。
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// 通过提供 "todoPage" 和 "settingsPage" 扩展基本测试。
// 这个新的 "test" 可以在多个测试文件中使用,每个文件都将获得 fixtures。
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
// 设置 fixture。
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// 在测试中使用 fixture 值。
await use(todoPage);
// 清理 fixture。
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
export { expect } from '@playwright/test';
// my-test.js
const base = require('@playwright/test');
const { TodoPage } = require('./todo-page');
const { SettingsPage } = require('./settings-page');
// 通过提供 "todoPage" 和 "settingsPage" 扩展基本测试。
// 这个新的 "test" 可以在多个测试文件中使用,每个文件都将获得 fixtures。
exports.test = base.test.extend({
todoPage: async ({ page }, use) => {
// 设置 fixture。
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// 在测试中使用 fixture 值。
await use(todoPage);
// 清理 fixture。
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
exports.expect = base.expect;
自定义 fixture 名称应以字母或下划线开头,并且只能包含字母、数字、下划线。
使用 Fixture
只需在测试函数参数中提及 fixture,测试运行器就会处理它。Fixtures 也可在钩子和其他 fixtures 中使用。如果您使用 TypeScript,fixtures 将具有正确的类型。
下面我们使用上面定义的 todoPage 和 settingsPage fixtures。
- TypeScript
- JavaScript
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.locator('.todo-item')).toContainText(['something nice']);
});
const { test, expect } = require('./my-test');
test.beforeEach(async ({ settingsPage }) => {
await settingsPage.switchToDarkMode();
});
test('basic test', async ({ todoPage, page }) => {
await todoPage.addToDo('something nice');
await expect(page.locator('.todo-item')).toContainText(['something nice']);
});
覆盖 Fixtures
除了创建自己的 fixtures 之外,您还可以覆盖现有 fixtures 以满足您的需求。考虑以下示例,它通过自动导航到某个 baseURL 来覆盖 page fixture:
- TypeScript
- JavaScript
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ baseURL, page }, use) => {
await page.goto(baseURL);
await use(page);
},
});
const base = require('@playwright/test');
exports.test = base.test.extend({
page: async ({ baseURL, page }, use) => {
await page.goto(baseURL);
await use(page);
},
});
请注意,在此示例中,page fixture 能够依赖其他内置 fixtures,例如 testOptions.baseURL。我们现在可以在配置文件中配置 baseURL,或在测试文件中使用 test.use(options) 在本地配置。
- TypeScript
- JavaScript
// example.spec.ts
test.use({ baseURL: 'https://playwright.dev' });
// example.spec.js
test.use({ baseURL: 'https://playwright.dev' });
Fixtures 也可以被覆盖,其中基本 fixture 完全被替换为不同的东西。例如,我们可以覆盖 testOptions.storageState fixture 以提供我们自己的数据。
- TypeScript
- JavaScript
import { test as base } from '@playwright/test';
export const test = base.extend({
storageState: async ({}, use) => {
const cookie = await getAuthCookie();
await use({ cookies: [cookie] });
},
});
const base = require('@playwright/test');
exports.test = base.test.extend({
storageState: async ({}, use) => {
const cookie = await getAuthCookie();
await use({ cookies: [cookie] });
},
});
Worker-scoped fixtures
Playwright Test 使用 worker 进程 来运行测试文件。与为单个测试运行设置测试 fixtures 类似,worker fixtures 为每个 worker 进程设置。这就是您可以设置服务、运行服务器等的地方。Playwright Test 将为尽可能多的测试文件重用 worker 进程,前提是它们的 worker fixtures 匹配,因此环境相同。
下面我们将创建一个 account fixture,它将由同一 worker 中的所有测试共享,并覆盖 page fixture 以便为每个测试登录到此帐户。为了生成唯一的帐户,我们将使用任何测试或 fixture 都可以使用的 workerInfo.workerIndex。注意 worker fixture 的类似元组的语法 - 我们必须传递 {scope: 'worker'},以便测试运行器为每个 worker 设置一次此 fixture。
- TypeScript
- JavaScript
// my-test.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.locator('#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.locator('#userinfo')).toHaveText(username);
// Use signed-in page in the test.
await use(page);
},
});
export { expect } from '@playwright/test';
// my-test.js
const base = require('@playwright/test');
exports.test = base.test.extend({
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.locator('#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.locator('#userinfo')).toHaveText(username);
// Use signed-in page in the test.
await use(page);
},
});
exports.expect = base.expect;
Automatic fixtures
自动 fixtures 为每个测试/worker 设置,即使测试没有直接列出它们。要创建自动 fixture,请使用元组语法并传递 { auto: true }。
这是一个自动附加调试日志的示例 fixture,当测试失败时,我们可以稍后在报告器中查看日志。注意它如何使用每个测试/fixture 中可用的 TestInfo 对象来检索有关正在运行的测试的元数据。
- TypeScript
- JavaScript
// my-test.ts
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';
// my-test.js
const debug = require('debug');
const fs = require('fs');
const base = require('@playwright/test');
exports.test = base.test.extend({
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 }],
});
Fixture timeout
默认情况下,fixture 与测试共享超时。但是,对于慢速 fixtures,尤其是 worker-scoped 的 fixtures,拥有单独的超时很方便。这样您可以保持整体测试超时较小,并为慢速 fixture 提供更多时间。
- TypeScript
- JavaScript
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 }) => {
// ...
});
const { test: base, expect } = require('@playwright/test');
const test = base.extend({
slowFixture: [async ({}, use) => {
// ... perform a slow operation ...
await use('hello');
}, { timeout: 60000 }]
});
test('example test', async ({ slowFixture }) => {
// ...
});
Fixtures-options
在版本 1.18 中,在配置文件中覆盖自定义 fixtures 已更改。了解更多。
Playwright Test 支持运行可以单独配置的多个测试项目。您可以使用"option" fixtures 使您的配置选项具有声明性和类型检查。了解有关 参数化测试 的更多信息。
下面我们将在其他示例的 todoPage fixture 之外创建一个 defaultItem 选项。此选项将在配置文件中设置。注意元组语法和 { option: true } 参数。
- TypeScript
- JavaScript
// my-test.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';
// my-test.js
const base = require('@playwright/test');
const { TodoPage } = require('./todo-page');
exports.test = base.test.extend({
// 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();
},
});
exports.expect = base.expect;
我们现在可以像往常一样使用 todoPage fixture,并在配置文件中设置 defaultItem 选项。
- TypeScript
- JavaScript
// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';
import { MyOptions } from './my-test';
const config: PlaywrightTestConfig<MyOptions> = {
projects: [
{
name: 'shopping',
use: { defaultItem: 'Buy milk' },
},
{
name: 'wellbeing',
use: { defaultItem: 'Exercise!' },
},
]
};
export default config;
// playwright.config.js
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig<{ defaultItem: string }>} */
const config = {
projects: [
{
name: 'shopping',
use: { defaultItem: 'Buy milk' },
},
{
name: 'wellbeing',
use: { defaultItem: 'Exercise!' },
},
]
};
module.exports = config;
Execution order
每个 fixture 都有一个由 fixture 中的 await use() 调用分隔的设置和拆卸阶段。设置在测试/钩子使用 fixture 之前执行,拆卸在测试/钩子不再使用 fixture 时执行。
Fixtures 遵循以下规则来确定执行顺序:
- 当 fixture A 依赖于 fixture B 时:B 总是在 A 之前设置,在 A 之后拆卸。
- 非自动 fixtures 延迟执行,仅当测试/钩子需要它们时才执行。
- 测试作用域的 fixtures 在每个测试后拆卸,而 worker 作用域的 fixtures 仅在执行测试的 worker 进程关闭时拆卸。
考虑以下示例:
- TypeScript
- JavaScript
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 () => { /* ... */ });
const { test: base } = require('@playwright/test');
const test = base.extend({
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 fixtures 总是在其他任何东西之前设置。beforeAll运行。
first test部分:autoTestFixture设置,因为自动测试 fixtures 总是在测试和beforeEach钩子之前设置。page设置,因为它在beforeEach钩子中需要。beforeEach运行。first test运行。afterEach运行。page拆卸,因为它是测试作用域的 fixture,应该在测试完成后拆卸。autoTestFixture拆卸,因为它是测试作用域的 fixture,应该在测试完成后拆卸。
second test部分:autoTestFixture设置,因为自动测试 fixtures 总是在测试和beforeEach钩子之前设置。page设置,因为它在beforeEach钩子中需要。beforeEach运行。workerFixture设置,因为它是second test所需的testFixture所需的。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为每个测试设置和拆卸,作为测试作用域的 fixtures。unusedFixture从未设置,因为它没有被任何测试/钩子使用。testFixture依赖于workerFixture并触发其设置。workerFixture在第二个测试之前延迟设置,但在 worker 关闭期间拆卸一次,作为 worker 作用域的 fixture。autoWorkerFixture为beforeAll钩子设置,但autoTestFixture不是。