Playwright 基础测试 #
测试的基本结构 #
test 函数 #
test 是 Playwright 中最基础的测试函数,用于定义一个测试用例:
typescript
import { test } from '@playwright/test';
test('测试描述', async ({ page }) => {
// 测试代码
});
第一个测试 #
typescript
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('首页标题测试', async ({ page }) => {
// 导航到页面
await page.goto('https://example.com');
// 验证标题
await expect(page).toHaveTitle(/Example Domain/);
});
test('页面内容测试', async ({ page }) => {
await page.goto('https://example.com');
// 验证元素存在
const heading = page.locator('h1');
await expect(heading).toContainText('Example Domain');
});
测试文件结构 #
text
tests/
├── example.spec.ts # .spec.ts 后缀
├── example.test.ts # 或 .test.ts 后缀
└── example.spec.js # JavaScript 文件
组织测试 #
describe 块 #
使用 test.describe 将相关测试分组:
typescript
import { test, expect } from '@playwright/test';
test.describe('用户认证', () => {
test.describe('登录功能', () => {
test('使用有效凭据登录', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
test('使用无效凭据登录失败', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'wrong@example.com');
await page.fill('#password', 'wrongpassword');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toBeVisible();
});
});
test.describe('注册功能', () => {
test('新用户注册', async ({ page }) => {
// 注册测试
});
});
});
嵌套 describe #
typescript
test.describe('电子商务', () => {
test.describe('购物车', () => {
test.describe('添加商品', () => {
test('添加单个商品', async ({ page }) => {
// 测试代码
});
test('添加多个商品', async ({ page }) => {
// 测试代码
});
});
test.describe('删除商品', () => {
test('删除单个商品', async ({ page }) => {
// 测试代码
});
});
});
});
测试输出结构 #
text
Running 5 tests using 1 worker
电子商务
购物车
添加商品
✓ 添加单个商品 (2s)
✓ 添加多个商品 (3s)
删除商品
✓ 删除单个商品 (1s)
5 passed (10s)
测试生命周期 #
钩子函数 #
Playwright 提供多种钩子函数:
typescript
import { test, expect } from '@playwright/test';
test.describe('测试生命周期', () => {
// 所有测试之前执行一次
test.beforeAll(async ({ browser }) => {
console.log('beforeAll - 所有测试之前');
});
// 每个测试之前执行
test.beforeEach(async ({ page }) => {
console.log('beforeEach - 每个测试之前');
await page.goto('/');
});
test('第一个测试', async ({ page }) => {
console.log('测试 1');
});
test('第二个测试', async ({ page }) => {
console.log('测试 2');
});
// 每个测试之后执行
test.afterEach(async ({ page }, testInfo) => {
console.log('afterEach - 每个测试之后');
console.log(`测试状态: ${testInfo.status}`);
});
// 所有测试之后执行一次
test.afterAll(async ({ browser }) => {
console.log('afterAll - 所有测试之后');
});
});
执行顺序 #
text
beforeAll
├── beforeEach
│ └── 测试 1
├── afterEach
├── beforeEach
│ └── 测试 2
├── afterEach
└── afterAll
钩子函数的实际应用 #
typescript
import { test, expect } from '@playwright/test';
test.describe('购物车测试', () => {
let cart;
test.beforeEach(async ({ page }) => {
// 每个测试前初始化购物车
await page.goto('/shop');
cart = page.locator('.cart');
});
test('添加商品到购物车', async ({ page }) => {
await page.click('.product:first-child .add-to-cart');
await expect(cart.locator('.item')).toHaveCount(1);
});
test('清空购物车', async ({ page }) => {
// 先添加商品
await page.click('.product:first-child .add-to-cart');
// 然后清空
await page.click('.clear-cart');
await expect(cart.locator('.item')).toHaveCount(0);
});
test.afterEach(async ({ page }, testInfo) => {
// 失败时截图
if (testInfo.status !== testInfo.expectedStatus) {
await page.screenshot({
path: `screenshots/${testInfo.title}.png`
});
}
});
});
测试夹具(Fixtures) #
内置夹具 #
Playwright 提供多个内置夹具:
typescript
import { test, expect } from '@playwright/test';
test('内置夹具示例', async ({
page, // Page 对象
context, // BrowserContext
browser, // Browser 实例
request, // APIRequestContext
browserName, // 当前浏览器名称
}) => {
// 使用 page
await page.goto('/');
// 使用 context
const newPage = await context.newPage();
// 使用 request
const response = await request.get('/api/users');
// 使用 browserName
console.log(`当前浏览器: ${browserName}`);
});
自定义夹具 #
typescript
// fixtures.ts
import { test as base } from '@playwright/test';
// 定义自定义夹具类型
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// 创建扩展测试
export const test = base.extend<MyFixtures>({
// 定义 todoPage 夹具
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage);
},
// 定义 settingsPage 夹具
settingsPage: async ({ page }, use) => {
const settingsPage = new SettingsPage(page);
await settingsPage.goto();
await use(settingsPage);
},
});
// 使用自定义夹具
test('使用自定义夹具', async ({ todoPage, settingsPage }) => {
await todoPage.addItem('Buy milk');
await todoPage.addItem('Buy bread');
await settingsPage.toggleDarkMode();
});
夹具的依赖关系 #
typescript
import { test as base } from '@playwright/test';
// 定义夹具
const test = base.extend<{
authenticatedPage: { page: Page; user: User };
}>({
authenticatedPage: async ({ page }, use) => {
// 设置:登录用户
await page.goto('/login');
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
const user = { id: 1, email: 'test@example.com' };
// 使用
await use({ page, user });
// 清理:登出用户
await page.click('#logout');
},
});
test('需要认证的测试', async ({ authenticatedPage }) => {
const { page, user } = authenticatedPage;
await page.goto('/profile');
await expect(page.locator('.email')).toHaveText(user.email);
});
跳过测试 #
skip 方法 #
typescript
import { test, expect } from '@playwright/test';
// 跳过单个测试
test.skip('这个测试被跳过', async ({ page }) => {
// 不会执行
});
// 条件跳过
test('条件跳过', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'WebKit 不支持此功能');
// 只在非 WebKit 浏览器执行
});
// 跳过整个 describe
test.describe.skip('这组测试被跳过', () => {
test('测试 1', async ({ page }) => {});
test('测试 2', async ({ page }) => {});
});
仅运行特定测试 #
typescript
// 只运行这一个测试
test.only('只运行这个测试', async ({ page }) => {
// 只有这个测试会运行
});
// 只运行这个 describe 块
test.describe.only('只运行这组测试', () => {
test('测试 1', async ({ page }) => {});
test('测试 2', async ({ page }) => {});
});
// 其他测试都会被跳过
test('这个测试会被跳过', async ({ page }) => {});
标记测试 #
typescript
// 标记为慢测试
test('慢测试', async ({ page }) => {
test.slow(); // 超时时间翻三倍
// 耗时操作
});
// 标记预期失败
test.fail('预期失败的测试', async ({ page }) => {
// 这个测试预期会失败
expect(true).toBe(false);
});
// 条件标记
test('条件失败', async ({ page, browserName }) => {
test.fail(browserName === 'firefox', 'Firefox 有已知问题');
// 在 Firefox 上预期失败
});
测试标签 #
添加标签 #
typescript
// 单个标签
test('测试 @smoke', async ({ page }) => {});
// 多个标签
test('测试', {
tag: ['@smoke', '@fast'],
}, async ({ page }) => {});
// describe 标签
test.describe('一组测试', {
tag: '@smoke',
}, () => {
test('测试 1', async ({ page }) => {});
test('测试 2', async ({ page }) => {});
});
按标签运行 #
bash
# 只运行带有 @smoke 标签的测试
npx playwright test --grep @smoke
# 排除带有 @slow 标签的测试
npx playwright test --grep-invert @slow
# 组合使用
npx playwright test --grep "@smoke|@fast"
测试注释 #
添加注释 #
typescript
test('带注释的测试', {
annotation: {
type: 'issue',
description: 'https://github.com/repo/issues/123',
},
}, async ({ page }) => {
// 测试代码
});
// 多个注释
test('多个注释', {
annotation: [
{ type: 'issue', description: 'https://github.com/repo/issues/123' },
{ type: 'docs', description: 'https://docs.example.com' },
],
}, async ({ page }) => {
// 测试代码
});
测试参数化 #
使用 test.describe.configure #
typescript
test.describe('参数化测试', () => {
// 配置模式
test.describe.configure({ mode: 'parallel' });
const users = [
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
{ name: 'Charlie', role: 'guest' },
];
for (const user of users) {
test(`用户 ${user.name} 的权限测试`, async ({ page }) => {
await page.goto('/profile');
await expect(page.locator('.role')).toHaveText(user.role);
});
}
});
使用 forEach #
typescript
const testCases = [
{ input: 'hello', expected: 'HELLO' },
{ input: 'world', expected: 'WORLD' },
{ input: 'Playwright', expected: 'PLAYWRIGHT' },
];
test.describe('大写转换测试', () => {
testCases.forEach(({ input, expected }) => {
test(`"${input}" 应该转换为 "${expected}"`, async ({ page }) => {
await page.goto('/');
await page.fill('#input', input);
await page.click('#convert');
await expect(page.locator('#output')).toHaveText(expected);
});
});
});
测试隔离 #
默认隔离 #
Playwright 默认为每个测试创建独立的上下文:
typescript
test.describe('测试隔离', () => {
test('测试 1', async ({ page }) => {
// page 是全新的
await page.goto('/');
await page.click('#login');
// 登录状态只在测试 1 中有效
});
test('测试 2', async ({ page }) => {
// page 是全新的,没有登录状态
await page.goto('/');
// 需要重新登录
});
});
共享上下文 #
typescript
test.describe.configure({ mode: 'serial' });
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
test('步骤 1: 登录', async () => {
await page.goto('/login');
await page.fill('#email', 'user@example.com');
await page.click('button[type="submit"]');
});
test('步骤 2: 访问仪表板', async () => {
// 使用相同的 page,保持登录状态
await page.goto('/dashboard');
await expect(page.locator('.welcome')).toBeVisible();
});
测试模式 #
AAA 模式 #
Arrange(准备)- Act(执行)- Assert(断言):
typescript
test('添加商品到购物车', async ({ page }) => {
// Arrange - 准备测试数据
const productName = 'Test Product';
const productPrice = 99.99;
// Act - 执行被测试的操作
await page.goto('/shop');
await page.click(`text=${productName}`);
await page.click('.add-to-cart');
// Assert - 验证结果
const cartItem = page.locator('.cart-item');
await expect(cartItem).toContainText(productName);
await expect(cartItem).toContainText(`$${productPrice}`);
});
Page Object 模式 #
typescript
// pages/LoginPage.ts
class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('#email', email);
await this.page.fill('#password', password);
await this.page.click('button[type="submit"]');
}
}
// tests/login.spec.ts
test('登录测试', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});
下一步 #
现在你已经掌握了 Playwright 基础测试的编写方法,接下来学习 元素定位 了解更多元素定位方式!
最后更新:2026-03-28