编译整理|TesterHome社区
作者|Ravi Kiran Vemula
以下为作者观点:
随着 Web 应用程序变得越来越复杂,维护干净、高效且可扩展的测试代码变得越来越具有挑战性。Playwright 是一个强大的端到端测试框架,它通过其夹具系统(fixture system)提供了解决方案。
本指南将引导大家了解利用 Playwright fixtures夹具创建强大且可维护的测试架构的高级技术。
什么是Playwright Fixture ?
Playwright 中的 Fixtures 可让你在测试之间共享数据或对象、设置先决条件并高效管理测试资源。它们还有助于减少代码重复并改善测试组织。
1. 创建页面对象Fixture
页面对象模型 (POM) 是一种设计模式,它在测试代码和页面特定代码之间创建了一个抽象层。让我们创建一些页面对象Fixture:
// pages/login.page.ts
import { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async login(username: string, password: string) {
await this.page.fill('#username', username);
await this.page.fill('#password', password);
await this.page.click('#login-button');
}
}
// pages/dashboard.page.ts
import { Page } from '@playwright/test';
export class DashboardPage {
constructor(private page: Page) {}
async getUserName() {
return this.page.textContent('.user-name');
}
}
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/login.page';
import { DashboardPage } from './pages/dashboard.page';
export const test = base.extend<{
loginPage: LoginPage;
dashboardPage: DashboardPage;
}>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
2. 创建API类 Fixture
API 类可用于直接与后端服务交互。创建 API 类Fixture的方法如下:
// api/user.api.ts
import { APIRequestContext } from '@playwright/test';
export class UserAPI {
constructor(private request: APIRequestContext) {}
async createUser(userData: any) {
return this.request.post('/api/users', { data: userData });
}
}
// api/product.api.ts
import { APIRequestContext } from '@playwright/test';
export class ProductAPI {
constructor(private request: APIRequestContext) {}
async getProducts() {
return this.request.get('/api/products');
}
}
// fixtures.ts
import { test as base } from '@playwright/test';
import { UserAPI } from './api/user.api';
import { ProductAPI } from './api/product.api';
export const test = base.extend<{
userAPI: UserAPI;
productAPI: ProductAPI;
}>({
userAPI: async ({ request }, use) => {
await use(new UserAPI(request));
},
productAPI: async ({ request }, use) => {
await use(new ProductAPI(request));
},
});
3. 在Worker范围内(Worker Scope)创建辅助Fixture
Playwright 中的 Worker 范围 Fixture 是一项强大的功能,可让实现在单个 Worker 进程中跨多个测试文件共享资源。这些 Fixture 对于设置成本高但可在多个测试中重复使用的操作(例如数据库连接或测试数据生成器)特别有用。
让我们探索如何创建和使用工作者范围的辅助Fixture:
// helpers/database.helper.ts
import { Pool } from 'pg';
export class DatabaseHelper {
private pool: Pool;
async connect() {
this.pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432'),
});
}
async query(sql: string, params: any[] = []) {
if (!this.pool) {
throw new Error('Database not connected. Call connect() first.');
}
const client = await this.pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release();
}
}
async disconnect() {
if (this.pool) {
await this.pool.end();
}
}
}
// helpers/test-data-generator.ts
import { faker } from '@faker-js/faker';
export class TestDataGenerator {
async init() {
// Any initialization logic here
console.log('TestDataGenerator initialized');
}
generateUser() {
return {
name: faker.person.fullName(),
email: faker.internet.email(),
password: faker.internet.password(),
};
}
generateProduct() {
return {
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
category: faker.commerce.department(),
};
}
}
// fixtures.ts
import { test as base } from '@playwright/test';
import { DatabaseHelper } from './helpers/database.helper';
import { TestDataGenerator } from './helpers/test-data-generator';
export const test = base.extend<
{},
{
dbHelper: DatabaseHelper;
testDataGen: TestDataGenerator;
}
>({
dbHelper: [async ({}, use) => {
const dbHelper = new DatabaseHelper();
await dbHelper.connect();
await use(dbHelper);
await dbHelper.disconnect();
}, { scope: 'worker' }],
testDataGen: [async ({}, use) => {
const testDataGen = new TestDataGenerator();
await testDataGen.init();
await use(testDataGen);
}, { scope: 'worker' }],
});
Worker 范围的 Fixture 几个优点:
效率:昂贵的设置操作(例如数据库连接)每个工作者执行一次,而不是每个测试执行一次。
资源共享:同一工作器内的多个测试可以共享相同的资源,从而减少总体资源消耗。
一致性:工作者内的所有测试都使用相同的Fixture实例,确保一致的状态和行为。
性能:通过重用连接和初始化对象,测试可以比为每个测试设置这些资源运行得更快。设备在使用后应拆除。
使用Worker Scope Fixture时的最佳实践:
使用Worker Scope来设置那些设置起来昂贵但可以在测试之间安全共享的Fixture。
确保Worker Scope的Fixture是无状态的或者可以在测试之间重置以防止测试相互依赖。
注意资源限制。虽然共享资源可以提高效率,但如果管理不当,也可能导致资源耗尽。
使用环境变量或配置文件来管理连接字符串和其他敏感数据。
需要注意的潜在陷阱:
测试隔离:通过修改共享状态确保使用Worker Scope Fixture的测试不会互相干扰。
资源泄漏:在夹具拆卸阶段正确管理资源,以防止泄漏。
下面是如何在测试中使用这些Worker Scope的夹具的示例:
// user.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';
test.describe('User management', () => {
test('list users', async ({ page, dbHelper }) => {
// The database is already connected and seeded with test data
await page.goto('/users');
const userCount = await page.locator('.user-item').count();
expect(userCount).toBeGreaterThan(0);
});
test('create new user', async ({ page, dbHelper }) => {
await page.goto('/users/new');
await page.fill('#name', 'New User');
await page.fill('#email', 'newuser@example.com');
await page.click('#submit');
// Verify the user was created in the database
const result = await dbHelper.client.query('SELECT * FROM users WHERE email = $1', ['newuser@example.com']);
expect(result.rows.length).toBe(1);
});
});
最佳实践
对于真正昂贵的操作,使用Worker Scope 的夹具,这些操作可以从跨测试共享中受益。
确保夹具自行清理,以防止测试污染。
使夹具能够灵活应对故障,实施适当的错误处理和记录。
考虑使用事务进行数据库操作,以便在每次测试后轻松回滚更改。
使用环境变量或配置文件来管理连接字符串和其他敏感数据。
实际应用
在大型应用中,可以使用Worker Scope Fixture来设置复杂的测试环境。这可能涉及启动多个服务、用大量测试数据填充数据库或执行耗时的身份验证过程。通过对每个工作器执行一次此操作,可以显著减少测试套件的总体运行时间。
4. 创建可选数据夹具(Data Fixtures)
可选Data Fixture提供了一种定义默认测试数据的方法,这些数据可以在特定测试中被覆盖。这种灵活性让您可以为测试设定一致的基准,同时还能适应特殊情况。
可选的Data Fixture有以下几个好处:
提供默认测试数据,减少在单个测试中设置数据的需要
允许轻松覆盖特定测试用例的数据
通过将测试数据与测试逻辑分离来提高测试的可读性
轻松管理测试套件中的不同数据场景
让我们扩展前面的例子并创建一个更全面的可选数据夹具:
// types/user.ts
export interface User {
username: string;
password: string;
email: string;
role: 'admin' | 'user';
}
// fixtures.ts
import { test as base } from '@playwright/test';
import { User } from './types/user';
export const test = base.extend<{
testUser?: User;
}>({
testUser: [async ({}, use) => {
await use({
username: 'defaultuser',
password: 'defaultpass123',
email: 'default@example.com',
role: 'user'
});
}, { option: true }],
});
现在,让我们在测试中使用这个夹具:
// user.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';
test.describe('User functionality', () => {
test('login with default user', async ({ page, testUser }) => {
await page.goto('/login');
await page.fill('#username', testUser.username);
await page.fill('#password', testUser.password);
await page.click('#login-button');
expect(page.url()).toContain('/dashboard');
});
test('admin user can access admin panel', async ({ page, testUser }) => {
test.use({
testUser: {
username: 'adminuser',
password: 'adminpass123',
email: 'admin@example.com',
role: 'admin'
}
});
await page.goto('/login');
await page.fill('#username', testUser.username);
await page.fill('#password', testUser.password);
await page.click('#login-button');
await page.click('#admin-panel');
expect(page.url()).toContain('/admin');
});
});
最佳实践
对在测试中常用但可能需要变化的数据使用可选的夹具。
保持默认数据简单且通用。针对特定场景使用覆盖。
考虑为不同的数据类别创建多个可选夹具(例如,,,testUser)。testProducttestOrder
使用 TypeScript 接口来确保测试数据的类型安全。
覆盖夹具时,仅指定需要更改的属性。剧作家会将覆盖与默认值合并。
真实场景
在电子商务应用中,你可能拥有不同的用户类型(访客、注册、高级)和产品类型(实物、数字、订阅)。可以为每种类型创建可选夹具,从而轻松测试各种场景,例如高级用户购买订阅产品,或访客用户购买实物。
5. 定义 Test Fixtures 和 Worker Fixtures 类型
类型化夹具利用 TypeScript 的类型系统在使用 Playwright 测试时提供更好的自动完成、类型检查和整体开发人员体验。
类型化夹具有几个优点:
通过 TypeScript 的静态类型检查提高代码完整性并减少错误
通过更好的自动完成和重构功能增强 IDE 支持
作为文档,明确说明每个夹具上有哪些属性和方法
通过类型交叉轻松组合复杂的测试设置
让我们使用类型化的夹具来创建一个更全面的设置:
// types.ts
import { LoginPage, ProductPage, CheckoutPage } from './pages';
import { UserAPI, ProductAPI, OrderAPI } from './api';
import { DatabaseHelper } from './helpers/database.helper';
import { User, Product, Order } from './models';
export interface PageFixtures {
loginPage: LoginPage;
productPage: ProductPage;
checkoutPage: CheckoutPage;
}
export interface APIFixtures {
userAPI: UserAPI;
productAPI: ProductAPI;
orderAPI: OrderAPI;
}
export interface HelperFixtures {
dbHelper: DatabaseHelper;
}
export interface DataFixtures {
testUser?: User;
testProduct?: Product;
testOrder?: Order;
}
export interface TestFixtures extends PageFixtures, APIFixtures, DataFixtures {}
export interface WorkerFixtures extends HelperFixtures {}
// basetest.ts
import { test as base } from '@playwright/test';
import { TestFixtures, WorkerFixtures } from './types';
export const test = base.extend<TestFixtures & WorkerFixtures>({
// Implement your fixtures here
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import { TestFixtures, WorkerFixtures } from './types';
export default defineConfig<TestFixtures, WorkerFixtures>({
use: {
baseURL: 'http://localhost:3000',
testUser: {
username: 'defaultuser',
password: 'defaultpass123',
email: 'default@example.com',
role: 'user'
},
// Other default fixture values
},
// ... other config options
});
现在,在编写测试时,可以获得完整的类型支持:
// checkout.spec.ts
import { test } from './basetest';
import { expect } from '@playwright/test';
test('complete checkout process', async ({
page,
loginPage,
productPage,
checkoutPage,
testUser,
testProduct,
orderAPI
}) => {
await loginPage.login(testUser.username, testUser.password);
await productPage.addToCart(testProduct.id);
await checkoutPage.completeCheckout();
const latestOrder = await orderAPI.getLatestOrderForUser(testUser.id);
expect(latestOrder.status).toBe('completed');
});
最佳实践
为不同类型的夹具(页面、API、数据等)定义清晰、独立的接口。
使用类型交叉来组成复杂的夹具设置。
在定义可选或子集夹具时,利用 TypeScript 的实用类型(如Partial<T>或Pick<T>)。
保持你的类型定义与你的实际实现同步。
使用严格的 TypeScript 设置来从类型检查中获得最大益处。
实际应用
在大型应用中,可能有数十个页面对象、API 客户端和数据模型。通过使用类型化夹具,可以确保测试套件的所有部分都能正确协同工作。例如,可以创建一个复杂的端到端测试,模拟跨多个页面的用户旅程、与各种 API 交互并在数据库中验证结果,所有这些都具有全类型安全性和自动完成支持。
组合不同类型的夹具
Playwright 夹具最强大的功能之一是能够组合不同类型的夹具来创建全面的测试设置。以下是将各种夹具类型组合在一起的示例:
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage, DashboardPage } from './pages';
import { UserAPI, ProductAPI } from './api';
import { DatabaseHelper } from './helpers/database.helper';
import { User, Product } from './types';
type TestFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
userAPI: UserAPI;
productAPI: ProductAPI;
testUser?: User;
testProduct?: Product;
};
type WorkerFixtures = {
dbHelper: DatabaseHelper;
};
export const test = base.extend<TestFixtures, WorkerFixtures>({
// Page object fixtures
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
// API fixtures
userAPI: async ({ request }, use) => {
await use(new UserAPI(request));
},
productAPI: async ({ request }, use) => {
await use(new ProductAPI(request));
},
// Optional data fixtures
testUser: [async ({}, use) => {
await use({ id: '1', username: 'testuser', email: 'test@example.com' });
}, { option: true }],
testProduct: [async ({}, use) => {
await use({ id: '1', name: 'Test Product', price: 9.99 });
}, { option: true }],
// Worker-scoped helper fixture
dbHelper: [async ({}, use) => {
const helper = new DatabaseHelper();
await helper.connect();
await helper.resetDatabase();
await use(helper);
await helper.disconnect();
}, { scope: 'worker' }],
});
现在你可以编写高度全面的测试:
// e2e.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('user can purchase a product', async ({
loginPage,
dashboardPage,
userAPI,
productAPI,
testUser,
testProduct,
dbHelper
}) => {
// Create a new user
const user = await userAPI.createUser(testUser);
// Log in
await loginPage.login(user.username, 'password123');
// Add product to cart
await dashboardPage.addToCart(testProduct.id);
// Complete purchase
await dashboardPage.completePurchase();
// Verify purchase in database
const dbOrder = await dbHelper.getLatestOrderForUser(user.id);
expect(dbOrder.productId).toBe(testProduct.id);
// Verify product stock updated
const updatedProduct = await productAPI.getProduct(testProduct.id);
expect(updatedProduct.stock).toBe(testProduct.stock - 1);
});
Bonus
合并测试和工作夹具(Worker Fixtures)
现在,让我们合并测试和工作夹具:
// fixtures.ts
import { test as base, mergeTests } from '@playwright/test';
import { TestFixtures, WorkerFixtures } from './types';
const testFixtures = base.extend<TestFixtures>({
// ... test fixtures implementation
});
const workerFixtures = base.extend<WorkerFixtures>({
// ... worker fixtures implementation
});
export const test = mergeTests(testFixtures, workerFixtures);
使用 TestFixture 和 WorkerFixture 类型扩展 basetest
为了给我们的测试提供正确的类型,我们可以扩展基础测试:
// basetest.ts
import { test as baseTest } from './fixtures.ts';
import { TestFixtures, WorkerFixtures } from './types';
export const test = baseTest.extend<TestFixtures, WorkerFixtures>({});
总结:有效使用Playwright Fixture的最佳实践建议
模块化你的Fixture(夹具):为不同的关注点(页面,API,数据等)创建单独的夹具,可以让测试代码保持井然有序且易于维护。
使用适当的范围:大多数情况下使用测试范围的夹具,并为真正昂贵的设置操作保留工作范围的夹具。
利用TypeScript:使用类型化夹具来提高代码完整性、减少错误并增强开发人员体验。
平衡灵活性和简单性:使用可选夹具提供默认数据,但不要使设置过于复杂。力求在灵活性和易用性之间取得良好的平衡。
保持夹具聚焦:每个夹具应承担单一职责。如果夹具承担的任务太多,可以考虑将其拆分为更小、更专注的夹具。
使用组合:组合不同类型的夹具来创建涵盖应用程序各个方面的综合测试设置。
保持一致性:在夹具中使用一致的命名约定和结构,使测试代码更具可读性和可维护性。
记录夹具:为夹具提供清晰的文档,特别是对于复杂的设置或在较大的团队中工作时。
定期重构:随着测试套件的增长,定期检查和重构夹具以确保它们保持高效和有效。
测试夹具:对于复杂的夹具,考虑为夹具本身编写测试以确保它们的行为符合预期。
通过遵循这些实践并充分利用 Playwright Fixture的全部功能,可以创建一个可随应用程序一起成长的强大、可维护且高效的测试套件。测试愉快!(原文链接:https://medium.com/@vrknetha/effective-utilization-of-playwright-fixtures-a-comprehensive-guide-841150525c7e)
2.原生鸿蒙,真正独立!部分应用只有基础功能,原因是必须进行大量稳定性测试?
3.实践分享|QA工程师如何利用生成式AI提高QA任务的生产力