嘿,朋友!刚入行测试或者正打算从手工测试转自动化?这篇文档就是为你准备的。不用慌,我会带你一步一步把整个流程走完,每一步都有可执行的代码和实际案例,读完后你就能独立完成一个完整的前端项目测试。
一、测试环境搭建:别跳过这一步
很多新手一上来就写脚本,结果跑不起来又不知道为啥。环境没搭好,后面全是坑。
1.1 选择你的技术栈
主流方案有三套,我全给你配齐:
| 方案 | 适用场景 | 学习曲线 |
|---|---|---|
| Playwright | 现代前端项目,Chrome/Firefox/Safari全覆盖 | 低 |
| Cypress | 快速验证,前端团队常用 | 低 |
| Selenium | 需要兼容老旧浏览器,企业级项目 | 中高 |
推荐新手直接上 Playwright,跨浏览器、有 IDE 插件、自动等待,比另外两个省心太多。
1.2 从零搭建 Playwright 环境
# 第一步:初始化项目
mkdir web-test && cd web-test
npm init -y
# 第二步:安装 Playwright 和 chromium 浏览器
npm install -D @playwright/test
npx playwright install chromium
# 如果要测 Firefox 和 WebKit,加这两行
npx playwright install firefox
npx playwright install webkit
# 第三步:生成配置文件
npx playwright init
跑完 npx playwright init 后,你会看到项目里多了一个 playwright.config.ts,结构大概这样:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true, // 并行跑,快很多
forbidOnly: !!process.env.CI, // CI 环境不允许有 skip 用例
retries: 0, // 本地允许重试
workers: process.env.CI ? 1 : undefined,
reporter: 'html', // 生成 HTML 报告
use: {
trace: 'on-first-retry', // 失败时自动录 trace
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
1.3 验证环境是否正常
写一个最简单的冒烟测试:
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('打开百度首页能正常显示', async ({ page }) => {
await page.goto('https://www.baidu.com');
await expect(page.title()).resolves.toContain('百度');
});
运行:
npx playwright test
# 或者只跑一个浏览器
npx playwright test --project=chromium
如果看到绿色 ✅ 通过,说明环境搭好了。
二、自动化脚本编写:从简单到复杂
2.1 基础操作:点击、输入、断言
import { test, expect } from '@playwright/test';
test('登录功能测试', async ({ page }) => {
// 打开登录页
await page.goto('https://example.com/login');
// 填写用户名和密码
await page.fill('#username', 'testuser');
await page.fill('#password', 'secret123');
// 点击登录按钮
await page.click('button[type="submit"]');
// 等待跳转到首页并断言
await page.waitForURL('**/dashboard');
await expect(page.locator('h1')).toContainText('欢迎回来');
});
核心 API 速查:
// 导航
await page.goto('https://xxx.com');
await page.reload();
await page.goBack();
// 元素操作
await page.click('#btn');
await page.fill('#input', '值');
await page.check('#checkbox'); // 勾选
await page.uncheck('#checkbox');
await page.selectOption('#select', 'option1');
// 断言
await expect(page.locator('#text')).toBeVisible();
await expect(page.locator('#text')).toContainText('hello');
await expect(page.locator('#input')).toHaveValue('预期值');
await expect(page).toHaveURL('**/success');
// 等待
await page.waitForSelector('.loading', { state: 'hidden' });
await page.waitForTimeout(1000); // 不推荐,用上面的
2.2 复杂场景:多页面跳转 + 文件上传
import { test, expect } from '@playwright/test';
test('上传头像并验证', async ({ page, browserName }) => {
await page.goto('https://example.com/profile');
// 等待文件上传控件出现
const fileInput = page.locator('input[type="file"]');
await fileInput.waitFor();
// 上传文件(支持多文件)
await fileInput.setInputFiles([
'tests/fixtures/avatar.png',
]);
// 等待预览图出现
await page.waitForSelector('.avatar-preview img');
// 断言文件名匹配
await expect(page.locator('.upload-status')).toContainText('avatar.png');
});
2.3 数据驱动测试:同一个脚本跑多组数据
import { test, expect } from '@playwright/test';
// 使用 test.describe 和 test.each 做多组数据
test.describe('不同用户登录场景', () => {
const loginCases = [
{ username: 'admin', password: 'admin123', expectRole: '管理员' },
{ username: 'user01', password: 'pass01', expectRole: '普通用户' },
{ username: 'user02', password: 'pass02', expectRole: '访客' },
];
test.each(loginCases)(
'用户 $username 登录后角色应为 $expectRole',
async ({ page, username, password, expectRole }) => {
await page.goto('https://example.com/login');
await page.fill('#username', username);
await page.fill('#password', password);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
// 断言角色标签
await expect(page.locator('.user-role')).toContainText(expectRole);
}
);
});
2.4 API 测试:前后端分离项目同样适用
import { test, expect } from '@playwright/test';
test('登录 API 返回正确结构', async ({ request }) => {
const response = await request.post('/api/login', {
data: {
username: 'testuser',
password: 'secret123',
},
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body).toMatchObject({
code: 0,
data: {
token: expect.any(String),
userId: expect.any(Number),
},
});
});
三、实战项目:完整测试一个电商网站
光看理论不够,我们来模拟一个完整的电商网站测试。
3.1 项目结构
ecommerce-test/
├── playwright.config.ts
├── tests/
│ ├── fixtures/
│ │ └── products.json
│ ├── login.spec.ts
│ ├── product.spec.ts
│ ├── cart.spec.ts
│ └── checkout.spec.ts
├── reports/ # HTML 报告自动输出到这里
└── package.json
3.2 测试用户登录
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
// 测试数据
const USERS = {
valid: { username: 'buyer01', password: 'Buyer@123' },
invalidUser: { username: 'wronguser', password: 'Buyer@123' },
invalidPass: { username: 'buyer01', password: 'wrongpass' },
};
test.beforeEach(async ({ page }) => {
await page.goto('https://shop.example.com/login');
});
test('有效用户登录成功', async ({ page }) => {
await page.fill('#username', USERS.valid.username);
await page.fill('#password', USERS.valid.password);
await page.click('button[type="submit"]');
await page.waitForURL('**/account');
await expect(page.locator('.user-welcome')).toContainText('buyer01');
});
test('用户名错误时显示错误提示', async ({ page }) => {
await page.fill('#username', USERS.invalidUser.username);
await page.fill('#password', USERS.invalidUser.password);
await page.click('button[type="submit"]');
await expect(page.locator('.error-message')).toContainText('用户名或密码错误');
});
test('密码错误时显示错误提示', async ({ page }) => {
await page.fill('#username', USERS.invalidPass.username);
await page.fill('#password', USERS.invalidPass.password);
await page.click('button[type="submit"]');
await expect(page.locator('.error-message')).toContainText('用户名或密码错误');
});
3.3 测试商品浏览和筛选
// tests/product.spec.ts
import { test, expect } from '@playwright/test';
test.describe('商品列表页', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://shop.example.com/products');
});
test('页面加载后显示商品列表', async ({ page }) => {
await expect(page.locator('.product-item')).toBeAttached({ count: 12 });
});
test('按分类筛选商品', async ({ page }) => {
await page.selectOption('#category-filter', 'electronics');
await page.waitForURL('**/products?category=electronics');
const products = page.locator('.product-item');
await expect(products).toBeAttached({ count: 6 });
});
test('搜索功能正常工作', async ({ page }) => {
await page.fill('#search-input', 'iPhone');
await page.press('#search-input', 'Enter');
await expect(page.locator('.product-item')).toContainText('iPhone');
});
test('排序功能', async ({ page }) => {
await page.selectOption('#sort-by', 'price-asc');
const prices = await page
.locator('.product-price')
.allTextContents()
.then(arr => arr.map(Number));
// 验证价格确实是升序
for (let i = 1; i < prices.length; i++) {
expect(prices[i]).toBeGreaterThanOrEqual(prices[i - 1]);
}
});
});
3.4 测试购物车全流程
// tests/cart.spec.ts
import { test, expect } from '@playwright/test';
test.describe('购物车流程', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://shop.example.com/products');
});
test('添加商品到购物车', async ({ page }) => {
await page.click('.product-item:first-child .add-to-cart');
await expect(page.locator('.cart-count')).toContainText('1');
});
test('修改商品数量', async ({ page }) => {
// 先加一个
await page.click('.product-item:first-child .add-to-cart');
await page.goto('https://shop.example.com/cart');
// 修改数量
await page.fill('#qty-1', '3');
await page.locator('#qty-1').press('Enter');
// 断言小计更新
await expect(page.locator('.line-total')).toContainText('¥');
});
test('从购物车移除商品', async ({ page }) => {
// 添加两个商品
await page.click('.product-item:first-child .add-to-cart');
await page.click('.product-item:last-child .add-to-cart');
await page.goto('https://shop.example.com/cart');
// 删除其中一个
await page.click('.remove-btn');
await expect(page.locator('.cart-item')).toHaveCount(1);
});
test('购物车为空时显示提示', async ({ page }) => {
await page.goto('https://shop.example.com/cart');
// 清空购物车
while ((await page.locator('.cart-item').count()) > 0) {
await page.click('.remove-btn');
}
await expect(page.locator('.empty-cart-msg')).toBeVisible();
});
});
四、解决常见 Bug:踩坑指南
4.1 Bug 1:元素找不到(NoSuchElementError)
症状: page.click('#btn') 报错,但页面上明明有这个按钮。
原因分析:
- 页面还没加载完就操作了
- 元素在 iframe 里
- 选择器写错了
- 元素被隐藏或不在可视区域
解决方案:
// ❌ 错误写法:直接操作,没有等待
await page.click('#submit-btn');
// ✅ 正确写法1:等待元素出现
await page.waitForSelector('#submit-btn', { state: 'visible' });
await page.click('#submit-btn');
// ✅ 正确写法2:用 expect 自动等待(推荐)
await expect(page.locator('#submit-btn')).toBeVisible();
await page.click('#submit-btn');
// ✅ 正确写法3:元素在 iframe 里
const frame = page.frameLocator('iframe#myframe');
await frame.locator('#submit-btn').click();
4.2 Bug 2:超时错误(TimeoutError)
症状: 测试跑到一半卡住,报 TimeoutError: waiting for selector。
解决方案:
// 在 playwright.config.ts 中调整超时时间
export default defineConfig({
timeout: 30000, // 默认 30 秒(原值 30000ms)
expect: {
timeout: 10000, // 断言超时 10 秒
},
// 全局操作超时
use: {
actionTimeout: 5000,
navigationTimeout: 30000,
},
});
// 单个用例单独调整
test('慢加载页面测试', async ({ page }) => {
await page.goto('https://slow-site.example.com', {
timeout: 60000, // 这个页面等 60 秒
});
});
4.3 Bug 3:元素被覆盖(Element is not clickable)
症状: 按钮在页面上,但点击时报 Element is not clickable at point (x, y)。
原因: 弹窗、广告、遮罩层挡住了目标元素。
解决方案:
// ❌ 直接点击会失败
await page.click('.modal-overlay .confirm-btn');
// ✅ 先关掉弹窗
await page.click('.close-btn');
await page.waitForSelector('.modal-overlay', { state: 'hidden' });
await page.click('.modal-overlay .confirm-btn');
// ✅ 或者强制点击(绕过可见性检查)
await page.click('.covered-btn', { force: true });
// ✅ 滚动到元素再操作
await page.locator('.far-away-btn').scrollIntoViewIfNeeded();
await page.click('.far-away-btn');
4.4 Bug 4:网络请求不稳定导致测试不稳定
症状: 同样的脚本,有时通过有时失败,没有逻辑错误。
解决方案:
// ✅ 拦截并 Mock 不稳定接口
test('订单提交', async ({ page }) => {
// 拦截支付接口,固定返回成功
await page.route('**/api/payment*', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ code: 0, msg: 'success' }),
});
});
await page.click('.submit-order');
await expect(page.locator('.payment-success')).toBeVisible();
});
// ✅ 等待所有网络请求完成再断言
await page.waitForLoadState('networkidle');
4.5 Bug 5:跨浏览器不一致
症状: Chrome 正常,Firefox 失败。
解决方案:
// 使用统一的视口大小
export default defineConfig({
use: {
viewport: { width: 1920, height: 1080 },
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
// 只跑某个浏览器
npx playwright test --project=firefox
六、兼容性测试:多浏览器、多设备
6.1 全浏览器并行测试
# 跑所有浏览器
npx playwright test
# 只跑移动设备
npx playwright test --project='Mobile Chrome'
npx playwright test --project='Mobile Safari'
# 只跑桌面浏览器
npx playwright test --project='Desktop Chrome'
6.2 设备模拟配置
”`typescript import { defineConfig, devices } from ‘@playwright/test’;
export default defineConfig({ projects: [
// 桌面
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Desktop Firefox',
use: { ...devices['Desktop Firefox'] },
},
// 移动端
