如何进行 TS 的 UI 自动化
技术选型
推荐 TypeScript + Playwright:
- 原生支持 TypeScript,类型提示好。
- 自动等待,减少 sleep。
- 多浏览器、多标签页、iframe、网络拦截都支持。
- 内置 trace、video、screenshot,失败定位方便。
- 支持 API 请求,方便用接口准备数据。
- 并行和分片能力好,适合 CI。
Cypress 也适合,但跨域、多标签页和浏览器支持不如 Playwright 灵活。
初始化
npm init -y
npm i -D @playwright/test typescript
npx playwright install
npx playwright test
playwright.config.ts:
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
timeout: 30_000,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html'], ['list']],
use: {
baseURL: 'https://staging.example.com',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
],
})
目录结构
tests/
auth/
login.spec.ts
orders/
create-order.spec.ts
pages/
LoginPage.ts
OrderPage.ts
fixtures/
auth.setup.ts
test-data.ts
utils/
api.ts
random.ts
playwright.config.ts
Page Object Model
import { Page, expect } from '@playwright/test'
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login')
}
async login(username: string, password: string) {
await this.page.getByLabel('账号').fill(username)
await this.page.getByLabel('密码').fill(password)
await this.page.getByRole('button', { name: '登录' }).click()
await expect(this.page.getByText('退出登录')).toBeVisible()
}
}
用例:
import { test, expect } from '@playwright/test'
import { LoginPage } from '../pages/LoginPage'
test('普通用户可以登录', async ({ page }) => {
const login = new LoginPage(page)
await login.goto()
await login.login('qa01', process.env.QA_PASSWORD!)
await expect(page).toHaveURL('/dashboard')
})
稳定性设计
1. 选择器
优先级:
getByRole:按可访问角色和名称。getByLabel/getByPlaceholder:表单。getByTestId:稳定的测试标识。- 最后才用 CSS/XPath。
不要依赖:
- 绝对 XPath
- 多层
nth-child - 会变化的文案
- 自动生成的 class
2. 自动等待
Playwright 的断言和操作会自动等待。不要写固定 sleep:
// 好
await expect(page.getByText('订单创建成功')).toBeVisible()
// 不好
await page.waitForTimeout(3000)
3. 登录态复用
用 storageState 保存登录态,避免每个用例都登录:
// fixtures/auth.setup.ts
import { test as setup } from '@playwright/test'
setup('登录', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('账号').fill('qa01')
await page.getByLabel('密码').fill(process.env.QA_PASSWORD!)
await page.getByRole('button', { name: '登录' }).click()
await page.context().storageState({ path: '.auth/user.json' })
})
// playwright.config.ts
use: { storageState: '.auth/user.json' }
4. 用 API 准备数据
UI 只验证关键流程,前置数据用接口创建:
const response = await request.post('/api/orders', {
data: { productId: 101, quantity: 1 },
})
expect(response.ok()).toBeTruthy()
5. 网络拦截
模拟异常、慢接口、权限失败:
await page.route('**/api/orders', route =>
route.fulfill({ status: 500, body: '{"message":"server error"}' })
)
6. 失败取证
trace: 'on-first-retry'- 失败截图和视频
- 控制台日志和网络请求
- 在 CI 中上传
playwright-report/和test-results/
7. 并行和隔离
- 每个用例独立数据和账号。
- 不要依赖执行顺序。
- 用 worker 级 fixture 管理账号池。
- 对共享资源加锁或使用独立租户。
CI 集成
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
最佳实践
- UI 自动化只覆盖关键用户旅程,业务逻辑用接口测试。
- 选择器稳定、语义化。
- 用例独立、可并行、可重复。
- 失败有 trace、截图、视频和日志。
- flaky 用例隔离并修复,不靠无限重试。
- 移动端和桌面端分 project 跑。
反模式
- 用
sleep等页面。 - 依赖绝对 XPath 和自动生成 class。
- 每个用例都从登录开始。
- 用 UI 创建所有测试数据。
- 用例之间共享账号和订单。
- 失败只看截图,不看 trace 和网络。
- 把所有业务校验都放在 UI 层。
面试回答模板
我会用 TypeScript + Playwright,按 Page Object 组织页面和操作,用 role、label、test-id 这类稳定选择器,依靠自动等待而不是 sleep。登录态用 storageState 复用,前置数据尽量用 API 创建,UI 只覆盖关键用户旅程。失败时开启 trace、截图和视频,CI 里并行执行并上传报告。最后把 flaky 用例隔离修复,而不是靠重试掩盖问题。