从 0 到 1 架构 Web + Mobile 端到端自动化测试平台:Playwright + Appium + TypeScript
当一个业务同时存在 Web、Android 和 iOS 三个客户端时,自动化测试最容易走向两个极端:
一种是三套测试工程各自发展,最终基础设施重复、测试数据重复、CI 重复;
另一种是为了“统一”而设计一个万能自动化框架,试图用同一套 Driver、Element API 同时操作浏览器和移动端,最后抽象层反而比业务代码更复杂。
本文介绍一种更适合中大型项目的架构:
执行引擎分开,业务能力共享,测试基础设施统一。
一、我们到底要解决什么问题?
假设一个产品同时拥有:
- Web 管理后台
- Web 用户端
- Android App
- iOS App
测试团队希望自动完成下面这些业务流程:
登录
↓
搜索商品
↓
加入购物车
↓
创建订单
↓
支付
↓
验证订单
这时候最直接的想法通常是:
Web 自动化
Android 自动化
iOS 自动化
然后分别建立三套工程。
短期没有问题。
但当测试 Case 从几十条增长到几百甚至几千条后,就会逐渐出现这些问题:
自动化测试规模扩大
│
┌────────────┼────────────┐
│ │ │
Web Android iOS
│ │ │
一套账号管理 一套账号管理 一套账号管理
一套测试数据 一套测试数据 一套测试数据
一套 API 一套 API 一套 API
一套日志 一套日志 一套日志
一套报告 一套报告 一套报告
问题本质不是:
Playwright 和 Appium 怎么一起用?
真正的问题其实是:
如何让不同终端的自动化测试共享业务能力和基础设施,但又保持各自最合适的执行模型?
二、技术选型
这套架构采用:
语言
└── TypeScript
Web
└── Playwright
Mobile
├── WebdriverIO
├── Appium
├── Android → UiAutomator2
└── iOS → XCUITest
工程管理
└── pnpm workspace
CI
├── Jenkins
├── GitLab CI
└── GitHub Actions
报告
├── Allure
├── JUnit
└── 自建测试报告平台
整体关系如下:
flowchart LR
A[TypeScript E2E Platform]
A --> B[Web Tests]
A --> C[Mobile Tests]
B --> D[Playwright]
D --> E[Chrome]
D --> F[Firefox]
D --> G[WebKit]
C --> H[WebdriverIO]
H --> I[Appium]
I --> J[UiAutomator2]
I --> K[XCUITest]
J --> L[Android]
K --> M[iOS]
为什么统一使用 TypeScript?
并不是因为 TypeScript 一定比其他语言更适合测试。
真正的价值是:
Web 和 Mobile 可以共享类型、配置、API Client、测试数据、账号池和业务模型。
三、不要构建“万能 Driver”
架构设计中最重要的一个原则是:
不要把 Playwright 和 Appium 强行抽象成同一个底层 Driver。
很多自动化框架一开始都会尝试做类似这样的东西:
class Element {
async click() {
if (platform === 'web') {
// Playwright click
}
if (platform === 'android') {
// Appium click
}
if (platform === 'ios') {
// XCUITest click
}
}
}
刚开始看起来非常优雅:
element.click();
element.input();
element.wait();
似乎 Web、Android、iOS 都统一了。
但随着业务复杂度提高,很快就会变成:
if Web
if Android
if iOS
if WebView
if Native
if Chrome
if Safari
if Element Scrollable
if System Permission
if Keyboard Visible
if Android Version
if iOS Version
...
最终得到的不是一个“统一自动化框架”。
而是:
一个比 Playwright 和 Appium 本身还复杂的中间层。
四、正确的抽象边界在哪里?
真正值得共享的是:
业务场景
测试数据
账号体系
API Client
环境配置
日志
报告
测试标签
CI 调度
而不应该强行共享:
Locator
Element
Page Driver
Gesture
Browser Context
Mobile Session
整体架构应该是:
flowchart TD
A[Business Scenario]
A --> B[Web Flow]
A --> C[Mobile Flow]
B --> D[Page Object]
C --> E[Screen Object]
D --> F[Playwright]
E --> G[WebdriverIO / Appium]
F --> H[Browser]
G --> I[Android / iOS]
一句话概括:
业务层统一,执行层隔离。
五、整体工程架构
推荐将整个自动化测试平台组织成 Monorepo。
e2e-automation/
│
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.base.json
│
├── packages/
│
│ ├── core/
│ │
│ │ └── src/
│ │ ├── config/
│ │ ├── api/
│ │ ├── data/
│ │ ├── accounts/
│ │ ├── logger/
│ │ ├── retry/
│ │ ├── assertions/
│ │ └── types/
│ │
│ ├── scenarios/
│ │
│ │ └── src/
│ │ ├── login/
│ │ ├── register/
│ │ ├── search/
│ │ ├── checkout/
│ │ └── profile/
│ │
│ ├── web-tests/
│ │
│ │ ├── playwright.config.ts
│ │ │
│ │ ├── src/
│ │ │ ├── pages/
│ │ │ ├── components/
│ │ │ ├── fixtures/
│ │ │ └── helpers/
│ │ │
│ │ └── tests/
│ │
│ └── mobile-tests/
│ │
│ ├── wdio.shared.conf.ts
│ ├── wdio.android.conf.ts
│ ├── wdio.ios.conf.ts
│ │
│ ├── src/
│ │ ├── screens/
│ │ ├── components/
│ │ ├── gestures/
│ │ ├── devices/
│ │ └── helpers/
│ │
│ └── tests/
│
├── config/
│ ├── env/
│ ├── devices/
│ └── test-suites/
│
├── scripts/
│ ├── run-web.ts
│ ├── run-mobile.ts
│ ├── prepare-test-data.ts
│ └── cleanup-test-data.ts
│
├── artifacts/
│ ├── screenshots/
│ ├── videos/
│ ├── traces/
│ └── logs/
│
└── reports/
这里最关键的是:
core
scenarios
这两个包不会关心:
Browser 是 Chrome 还是 Safari
手机是 Android 还是 iPhone
底层是 Playwright 还是 Appium
它们主要负责:
环境
账号
测试数据
API
业务模型
日志
报告
六、Web:Page Object
Web 使用 Playwright。
例如登录页:
import { Page } from '@playwright/test';
export class LoginPage {
constructor(private readonly page: Page) {}
username = this.page.getByTestId('username');
password = this.page.getByTestId('password');
submit = this.page.getByTestId('login-button');
async login(username: string, password: string) {
await this.username.fill(username);
await this.password.fill(password);
await this.submit.click();
}
}
页面对象主要负责:
Locator
UI Operation
Page Behavior
而不要承担业务流程。
例如下面这种代码就不建议放入 LoginPage:
async loginAndCreateOrder() {
// ...
}
因为:
Page Object 表示页面能力,不应该表示完整业务流程。
七、Mobile:Screen Object
Mobile 使用:
WebdriverIO
↓
Appium
↓
Android / iOS
登录页面可以写成:
export class LoginScreen {
get username() {
return $('~username');
}
get password() {
return $('~password');
}
get submit() {
return $('~login-button');
}
async login(username: string, password: string) {
await this.username.setValue(username);
await this.password.setValue(password);
await this.submit.click();
}
}
Android 和 iOS 如果 UI 结构高度一致,可以共享部分 Screen。
但如果两端差异比较大,建议直接拆开:
screens/
├── android/
│ ├── LoginScreen.ts
│ └── CheckoutScreen.ts
│
└── ios/
├── LoginScreen.ts
└── CheckoutScreen.ts
不要为了“代码复用率”牺牲可维护性。
八、真正需要共享的是 Business Flow
假设我们有一个登录流程:
export interface LoginFlow {
login(user: TestUser): Promise<void>;
}
Web 实现:
export class WebLoginFlow implements LoginFlow {
constructor(
private readonly loginPage: LoginPage
) {}
async login(user: TestUser) {
await this.loginPage.login(
user.username,
user.password
);
}
}
Mobile 实现:
export class MobileLoginFlow implements LoginFlow {
constructor(
private readonly loginScreen: LoginScreen
) {}
async login(user: TestUser) {
await this.loginScreen.login(
user.username,
user.password
);
}
}
于是业务测试可以变成:
await loginFlow.login(user);
await searchFlow.search('MacBook');
await cartFlow.addProduct();
const order = await checkoutFlow.checkout();
expect(order.success).toBe(true);
测试代码关注的是:
用户做了什么
而不是:
元素怎么点
元素怎么查
页面怎么滚
九、一条测试到底是怎么运行的?
整个执行链路如下。
flowchart TD
A[开始测试]
A --> B[加载运行环境]
B --> C[创建测试账号]
C --> D[准备测试数据]
D --> E{测试平台}
E -->|Web| F[启动 Playwright]
E -->|Android| G[启动 Appium Android Session]
E -->|iOS| H[启动 Appium iOS Session]
F --> I[执行业务场景]
G --> I
H --> I
I --> J[UI Assertion]
J --> K[API / DB 验证]
K --> L{是否成功}
L -->|成功| M[记录测试结果]
L -->|失败| N[截图 / 视频 / 日志 / Trace]
N --> M
M --> O[清理测试数据]
O --> P[生成测试报告]
这里有一个非常重要的理念:
自动化测试不是单纯的 UI 操作。
完整测试实际上是:
API
+
Test Data
+
UI
+
Assertion
+
Logging
+
Reporting
十、测试数据必须成为一等公民
很多 E2E 自动化项目最后不稳定,并不是 Playwright 不稳定。
也不是 Appium 不稳定。
而是:
测试数据不稳定。
例如:
test('用户可以购买商品', async () => {
await login('test001', '123456');
});
这种代码隐藏了大量问题:
这个账号还存在吗?
余额够吗?
是否被其他测试占用?
是否已经购买过?
有没有被封?
环境数据是否一致?
更合理的方式是:
const user = await testData.createUser({
balance: 10000,
status: 'active'
});
然后:
await loginFlow.login(user);
测试完成以后:
await testData.cleanup(user);
测试生命周期应该变成:
flowchart LR
A[Test Case]
A --> B[API 创建用户]
B --> C[API 创建商品]
C --> D[API 创建测试订单数据]
D --> E[UI 执行核心业务]
E --> F[API / DB 验证结果]
F --> G[清理测试数据]
这样 UI 自动化只验证:
必须通过 UI 才能验证的内容。
十一、账号池设计
当 CI 开始并行运行以后,会出现另一个问题:
Test 01 ─┐
Test 02 ─┼── test001
Test 03 ─┘
三个 Case 同时使用一个账号。
后果通常是:
状态污染
订单冲突
购物车冲突
登录 Session 冲突
数据互相覆盖
因此建议设计:
Account Pool
运行时:
flowchart LR
A[Test Worker 1] --> D[Account Pool]
B[Test Worker 2] --> D
C[Test Worker 3] --> D
D --> E[test001]
D --> F[test002]
D --> G[test003]
基本接口可以是:
const account = await accountPool.acquire({
type: 'normal-user'
});
try {
await runTest(account);
} finally {
await accountPool.release(account);
}
这样才能真正支持稳定并发。
十二、Web 浏览器矩阵
Web 可以通过 Playwright Projects 定义浏览器矩阵:
export default defineConfig({
projects: [
{
name: 'chrome',
use: {
...devices['Desktop Chrome']
}
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox']
}
},
{
name: 'webkit',
use: {
...devices['Desktop Safari']
}
}
]
});
执行关系:
flowchart LR
A[Web Test]
A --> B[Chrome]
A --> C[Firefox]
A --> D[WebKit]
B --> E[Result]
C --> E
D --> E
但并不意味着所有 PR 都要跑所有浏览器。
测试策略应该分层。
十三、Mobile 设备矩阵
Mobile 与 Web 最大的区别是:
Web 并发资源通常是:
CPU
Memory
Browser Worker
而 Mobile 真正受限制的是:
Device
Simulator
Emulator
因此应该构建设备池:
flowchart TD
A[Mobile Test Queue]
A --> B[Device Scheduler]
B --> C[Android 01]
B --> D[Android 02]
B --> E[iPhone 01]
B --> F[iPhone 02]
C --> G[Test Case]
D --> G
E --> G
F --> G
设备元数据建议维护:
interface Device {
id: string;
platform: 'android' | 'ios';
version: string;
model: string;
status: 'idle' | 'busy' | 'offline';
}
以后可以进一步扩展成 Device Farm。
十四、测试标签体系
当 Case 达到几百条以后,不能再依靠目录决定执行策略。
建议采用 Tag。
例如:
运行级别
@smoke
@regression
优先级
@p0
@p1
@p2
业务
@login
@checkout
@payment
@search
平台
@web
@android
@ios
环境
@staging
@production-safe
例如:
test(
'@p0 @smoke @checkout 用户可以完成购买',
async () => {
// ...
}
);
然后执行:
pnpm test --tag @smoke
或者:
pnpm test --tag "@p0 && @checkout"
进一步还可以实现:
pnpm test:web --tag @regression
以及:
pnpm test:android --tag @smoke
十五、CI 怎么设计?
不要每次 Pull Request 都执行全部 E2E。
假设:
Web Case 300
Android Case 200
iOS Case 200
如果全部执行:
700 Cases × 多浏览器 × 多设备
整个 Pipeline 会非常昂贵。
建议采用三级测试策略。
flowchart TD
A[Pull Request]
A --> B[P0 Smoke]
B -->|Pass| C[Merge]
B -->|Fail| D[Block Merge]
C --> E[Core Regression]
E --> F[Nightly]
F --> G[Full Matrix]
G --> H[Chrome]
G --> I[Firefox]
G --> J[WebKit]
G --> K[Android Devices]
G --> L[iOS Devices]
推荐:
Pull Request
P0
Smoke
核心链路
例如:
登录
搜索
下单
支付
目标:
5 ~ 15 分钟
Merge
执行:
Core Regression
例如:
核心业务
核心页面
主要设备
Nightly
每天晚上执行:
Full Regression
包括:
Chrome
Firefox
Safari
Android 多版本
iOS 多版本
十六、报告体系
报告不能只有:
Passed
Failed
一个成熟的 E2E 平台应该记录:
Test Name
Platform
Environment
Browser
Device
App Version
Git Commit
Start Time
Duration
Screenshot
Video
Trace
Console Log
Appium Log
Network Log
Error Stack
最终报告可以呈现:
Run #10231
──────────────────────────────────
Web
Chrome 145 / 147
Firefox 142 / 147
WebKit 144 / 147
Android
Pixel 81 / 83
Samsung 79 / 83
iOS
iPhone 15 82 / 83
──────────────────────────────────
Passed 573
Failed 11
Skipped 6
整体流程:
flowchart LR
A[Playwright Result] --> D[Report Aggregator]
B[Android Result] --> D
C[iOS Result] --> D
D --> E[JUnit]
D --> F[Allure]
D --> G[Database]
G --> H[Trend Dashboard]
十七、失败的时候到底保存什么?
一个失败 Case 至少应该留下:
Screenshot
Video
Error Stack
Console Log
Device Log
Web 还可以保存:
Playwright Trace
Network
Browser Console
DOM Snapshot
Mobile 可以保存:
Appium Server Log
Device Log
Screen Recording
Page Source
理想状态是开发者看到:
Checkout failed
之后,不需要 QA 手工复现。
而是可以直接看到:
哪一步失败
失败时页面长什么样
前端报了什么错
接口有没有失败
设备有没有异常
十八、Flaky Test 怎么处理?
E2E 自动化最大的敌人不是 Fail。
而是:
Flaky。
也就是:
第一次失败
第二次通过
第三次又失败
如果简单粗暴地设置:
retry = 3
很多问题会被掩盖。
更合理的方法:
flowchart TD
A[Test Failed]
A --> B[Retry]
B --> C{Retry Result}
C -->|Fail| D[Real Failure]
C -->|Pass| E[Mark Flaky]
E --> F[Flaky Database]
F --> G[Trend Analysis]
G --> H{超过阈值?}
H -->|Yes| I[创建治理任务]
H -->|No| J[继续观察]
建议给每一个 Case 维护:
Owner
Failure Rate
Flaky Rate
Average Duration
Last Failure
甚至可以设置:
Flaky > 5%
自动通知负责人。
十九、完整自动化测试平台
随着能力逐步增加,最终整个测试平台会演进成:
flowchart TB
A[Developer / QA]
A --> B[Test CLI]
B --> C[Test Orchestrator]
C --> D[Web Runner]
C --> E[Mobile Scheduler]
D --> F[Playwright]
E --> G[Android Device Pool]
E --> H[iOS Device Pool]
F --> I[Business Scenario]
G --> I
H --> I
I --> J[Test Infrastructure]
J --> K[Account Pool]
J --> L[Test Data]
J --> M[API Client]
J --> N[Config]
J --> O[Logger]
I --> P[Test Artifacts]
P --> Q[Screenshot]
P --> R[Video]
P --> S[Trace]
P --> T[Logs]
Q --> U[Report Platform]
R --> U
S --> U
T --> U
U --> V[Trend Analysis]
U --> W[Flaky Analysis]
U --> X[Alert]
这时候它已经不再只是一个:
自动化脚本仓库
而是一个真正的:
E2E Automation Platform
二十、推荐实施路线
如果从 0 开始,不建议一上来就做:
设备云
测试管理后台
账号中心
报表中心
调度中心
Flaky AI 分析
第一阶段最重要的是:
跑通闭环。
Phase 1:基础能力
先实现:
Playwright
Appium
TypeScript
pnpm workspace
然后选三个 Case:
Login
Search
Checkout
要求:
Web ✓
Android ✓
iOS ✓
Phase 2:基础设施
增加:
Test Data Factory
Account Pool
API Client
Environment Config
Logging
Artifacts
Phase 3:CI
实现:
PR Smoke
Merge Regression
Nightly Full Regression
Phase 4:设备池
逐渐支持:
Android Emulator
Android Real Device
iOS Simulator
iOS Real Device
Phase 5:平台化
最后才进入:
Report Platform
Device Farm
Flaky Dashboard
Trend Analysis
Quality Dashboard
Alert
Case Owner
整个演进路线:
flowchart LR
A[脚本]
--> B[工程化]
--> C[基础设施]
--> D[CI]
--> E[设备池]
--> F[报告平台]
--> G[质量平台]
二十一、三个最值得坚持的设计原则
最后总结一下这套架构中最重要的三个原则。
1. 执行引擎分开
Web
↓
Playwright
Mobile
↓
Appium
不要为了统一而统一。
2. 业务能力共享
共享:
Scenario
Test Data
API
Account
Config
Logger
Report
而不是:
Element
Driver
Locator
3. UI 只负责验证 UI
能够通过 API 完成的:
创建用户
创建订单
创建商品
初始化数据
清理数据
尽量不要通过 UI 做。
UI 自动化应该把时间投入到:
用户真正看到和操作的关键路径
二十二、最终架构
最终推荐的结构可以总结为:
E2E Automation Platform
│
┌───────────┴───────────┐
│ │
Playwright Appium
│ │
Web WebdriverIO
│
┌──────┴──────┐
│ │
Android iOS
UiAutomator2 XCUITest
──────────────────────────────────────────────
Shared Infrastructure
──────────────────────────────────────────────
TypeScript
Business Scenario
Test Data Factory
Account Pool
API Client
Environment Config
Logging
Reporting
Tagging
Artifacts
CI Orchestrator
Device Scheduler
架构原则可以浓缩成一句话:
执行引擎分开,业务能力共享,基础设施统一。
当项目只有十几条自动化 Case 时,架构可能并不重要。
但当 Case 达到:
100
500
1000+
真正决定这个项目能不能继续维护的,已经不是:
某一个 Locator 怎么写
而是:
测试数据如何管理?
设备如何调度?
测试如何并发?
失败如何定位?
Flaky 如何治理?
多平台如何共享业务能力?
CI 如何控制执行成本?
这也是自动化测试从:
“写脚本”
走向:
“工程化”
再走向:
“平台化”
最关键的一步。
评论