ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

e2e测试从入门到精通:工具选型、框架搭建与稳定性治理实战

e2e测试从入门到精通:工具选型、框架搭建与稳定性治理实战

1. 从“e2e”这个标题说起:它到底指什么

第一次看到“e2e”这三个字符,很多人会愣一下。它不像“Docker”“Kubernetes”那样一眼能看出领域,也不像“React”“Vue”那样自带技术栈标签。但如果你在软件工程、测试、通信或者产品交付这些圈子里待过一段时间,就会知道“e2e”几乎是绕不开的一个缩写——end-to-end,端到端。

我最早接触这个词是在做Web项目的时候。当时团队里有人提了一句“这个流程得补个e2e”,我第一反应是“什么e2e?”后来才明白,人家说的是端到端测试。再后来做数据管道、做API集成、做移动端自动化,发现“e2e”这个词出现的频率越来越高,而且每次出现的语境都不太一样。有时候它指的是一种测试类型,有时候它指的是一种架构视角,有时候它甚至是一种思维方式——从最前端的用户操作,一路追踪到最末端的数据库落盘,中间不跳过任何环节。

所以这篇博文,我想把“e2e”这个看似简单却内涵丰富的词彻底拆开。不管你是刚入行的测试新人,还是写了几年业务代码想补测试覆盖的老手,或者是对系统全链路质量保障感兴趣的技术管理者,都能从这里找到可以直接拿走用的东西。我会从概念本身讲起,然后落到实操,再把我自己踩过的坑和总结出来的经验一并倒出来。文章会比较长,因为“e2e”这件事本身就不是三言两语能说清的,它牵扯到工具选型、环境治理、数据准备、断言策略、稳定性优化等一系列工程问题。

先给一个最朴素的定位:e2e测试是一种从用户视角出发、覆盖完整业务链路的自动化验证手段。它不关心某个函数内部怎么实现,也不关心某个接口返回的字段是不是符合预期,它只关心一件事——用户从进入系统到完成目标,这条路上所有环节是不是都能正常工作。这个定义听起来简单,但真正落地的时候,你会发现“所有环节”这四个字背后藏着无数细节。

2. e2e的核心价值与适用边界

2.1 为什么单元测试和集成测试不够用

很多团队在测试金字塔的底层做得不错,单元测试覆盖率能到80%以上,集成测试也有一批。但上线之后还是会出现“点某个按钮没反应”“提交表单之后页面白屏”“支付成功但订单状态没更新”这类问题。原因很简单:单元测试验证的是函数级别的正确性,集成测试验证的是模块之间的接口契约,它们都不负责验证“用户真实操作路径”的完整性。

我举个实际例子。之前做一个电商项目,商品详情页有一个“加入购物车”按钮。单元测试覆盖了购物车服务的addItem方法,集成测试覆盖了商品服务和购物车服务之间的调用。但上线后发现,在iOS Safari上点击按钮之后,因为某个CSS层级问题,按钮的点击事件被一个透明遮罩拦截了。单元测试和集成测试都发现不了这个问题,因为它们的运行环境里根本没有浏览器渲染这一层。只有e2e测试,用真实浏览器打开页面、真实点击、真实断言,才能捕获这类问题。

这就是e2e的核心价值:它验证的是“用户能感知到的正确性”,而不是代码层面的正确性。两者之间的差距,往往就是线上事故和用户投诉的来源。

2.2 e2e不是银弹:什么时候不该用它

但我也见过一些团队把e2e当成万能药,恨不得所有场景都用e2e覆盖。结果就是测试套件跑一次要四十分钟,三天两头因为环境抖动挂掉,维护成本高到没人愿意碰。所以这里必须说清楚e2e的适用边界。

适合用e2e的场景:核心业务流程(注册登录、下单支付、内容发布)、跨系统交互(前端到后端到第三方服务)、浏览器兼容性验证、关键用户路径的回归验证。

不适合用e2e的场景:纯算法逻辑、工具函数、样式细节、边界条件极多的数据校验。这些应该交给单元测试和集成测试。e2e的定位是“少而精”,不是“多而全”。

我个人的经验法则是:e2e测试用例的数量应该控制在总测试用例的10%到15%之间。如果一个团队的e2e用例超过30%,那大概率是把太多本该在底层验证的东西推到了顶层,维护成本会指数级上升。

2.3 e2e在持续交付流水线中的位置

在CI/CD流水线里,e2e通常放在比较靠后的阶段。前面是代码检查、单元测试、构建、集成测试,最后才是e2e。这个顺序不是随便定的,因为e2e的运行成本最高、耗时最长、稳定性最差。如果前面的快速反馈环节能拦住问题,就不需要浪费资源去跑e2e。

但这也带来一个矛盾:如果e2e只在最后跑,那它反馈周期就长,开发者在提交代码后要等很久才知道有没有破坏端到端流程。我的做法是分两层:冒烟级e2e在每次合并请求时触发,只跑最核心的三五条路径,控制在五分钟以内;全量e2e在每日构建或者发布前触发,覆盖所有关键业务流程。这样既保证了快速反馈,又保证了发布前的全面验证。

3. e2e测试工具选型:别只看流行度

3.1 主流工具横向对比

选e2e工具这件事,我踩过不少坑。早年用Selenium,后来试过TestCafe、Cypress、Playwright、Puppeteer,每个工具都有自己的脾气。下面这张表是我根据实际使用体验整理的对比,不是官方文档的复述,而是带着“用起来到底怎么样”的主观判断。

工具语言支持浏览器支持自动等待网络拦截多标签页学习曲线我的评价
Selenium多语言最全弱需额外库支持陡老牌但配置繁琐,适合遗留系统
CypressJS/TSChromium系+Firefox强内置有限平缓开发体验好,但跨域和多标签受限
PlaywrightJS/TS/Python/Java/.NETChromium/Firefox/WebKit很强内置完善中等目前综合最强,推荐新项目首选
PuppeteerJS/TSChromium中等内置支持平缓偏底层,适合做定制化采集
TestCafeJS/TS多浏览器强内置支持平缓无需WebDriver,但生态相对小

如果让我给一个直接的建议:新项目直接上Playwright。它的自动等待机制最省心,网络拦截API设计得最干净,多浏览器支持也最完整。Cypress适合前端团队快速上手,但遇到跨域和多标签场景会比较难受。Selenium除非是维护老项目,否则不建议新开坑。

3.2 选型时容易忽略的三个维度

除了功能对比,还有几个维度是选型时容易忽略但后期会狠狠折磨你的。

第一个是调试体验。e2e测试挂掉的时候,你能不能快速定位是选择器写错了、接口返回慢了、还是页面渲染时机不对?Playwright的Trace Viewer和Cypress的时间旅行调试都做得不错,但Selenium在这块就比较原始。调试体验直接决定了你修一个失败用例要花五分钟还是五十分钟。

第二个是并行执行能力。当你的e2e用例超过五十条,串行执行的时间就会变得不可接受。工具是否原生支持分片并行、是否支持在CI中动态分配worker,这个能力很关键。Playwright的shard机制和Cypress的parallelization都需要额外配置,但Playwright的配置更直观。

第三个是社区活跃度和更新频率。e2e工具和浏览器版本强绑定,浏览器一更新,工具就得跟着适配。如果一个工具半年不更新,大概率会在某次浏览器升级后出问题。选型时去GitHub看一眼最近的commit频率和issue响应速度,比看star数更有用。

3.3 我的最终技术栈组合

经过几轮折腾,我目前稳定使用的组合是:Playwright + TypeScript + Allure报告 + GitHub Actions(或Jenkins)。TypeScript提供类型提示,写选择器和断言的时候不容易拼错;Allure报告的可读性比Playwright自带的HTML报告更好,适合给非技术同学看;CI层面用容器化运行,保证环境一致性。

这个组合不是唯一解,但对我来说是维护成本、调试效率、报告可读性三者平衡得比较好的方案。你可以根据自己的团队情况调整,但核心原则是:工具要服务于流程,而不是让流程去迁就工具。

4. 从零搭建e2e测试框架的实操步骤

4.1 项目初始化与依赖安装

假设我们从零开始,用Playwright搭一个e2e框架。第一步是初始化项目。

mkdir e2e-demo && cd e2e-demo npm init -y npm install -D @playwright/test typescript ts-node npx playwright install

这几条命令做完之后,Playwright的浏览器二进制文件就装好了。注意npx playwright install会下载Chromium、Firefox、WebKit三个浏览器,如果CI环境磁盘空间紧张,可以只装需要的,比如npx playwright install chromium。

接下来初始化TypeScript配置:

npx tsc --init

然后在tsconfig.json里确保strict模式打开,target设为ES2020以上。这些配置看起来琐碎,但类型检查能在你写选择器的时候提前发现拼写错误,省下大量调试时间。

4.2 目录结构设计与配置拆分

一个可维护的e2e项目,目录结构不能太随意。我通常这样组织:

e2e-demo/ ├── tests/ # 测试用例 │ ├── smoke/ # 冒烟用例 │ └── regression/ # 回归用例 ├── pages/ # 页面对象 ├── fixtures/ # 测试数据 ├── utils/ # 工具函数 ├── playwright.config.ts # 全局配置 └── package.json

pages目录放页面对象(Page Object),把页面元素和操作封装成类,测试用例里只调用方法,不直接写选择器。这样做的好处是页面改版时只需要改页面对象,不用改几十个测试文件。

fixtures目录放测试数据,比如用户账号、商品ID、订单号。这些数据最好通过API动态创建,而不是写死在代码里,否则测试之间会互相污染。

playwright.config.ts是核心配置文件,下面是一个我常用的模板:

import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', timeout: 30000, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 4 : undefined, reporter: [['html'], ['allure-playwright']], use: { baseURL: process.env.BASE_URL || 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, ], });

这里有几个关键配置值得解释。retries在CI环境设为2,本地设为0,因为本地调试时你希望失败就是失败,不要自动重试掩盖问题。trace设为on-first-retry,意思是第一次重试时记录完整追踪,这样既节省资源,又能在需要时拿到详细日志。screenshot和video都设为只在失败时保留,避免每次运行都产生大量无用文件。

4.3 页面对象模式的实际应用

页面对象模式是e2e框架可维护性的基石。我见过太多项目把选择器直接写在测试用例里,结果页面一改版,几十个用例全挂。下面是一个登录页的页面对象示例:

import { Page, Locator } from '@playwright/test'; export class LoginPage { readonly page: Page; readonly usernameInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.usernameInput = page.getByLabel('用户名'); this.passwordInput = page.getByLabel('密码'); this.submitButton = page.getByRole('button', { name: '登录' }); this.errorMessage = page.getByTestId('login-error'); } async goto() { await this.page.goto('/login'); } async login(username: string, password: string) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.submitButton.click(); } }

注意这里选择器的写法。我优先用getByLabel、getByRole、getByTestId这些语义化选择器,而不是page.locator('.login-btn')这种CSS类选择器。原因是CSS类名经常因为样式重构而改变,但role和label是面向可访问性的,相对稳定。getByTestId需要前端配合加>import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; test.describe('登录功能', () => { test('正确账号密码可以登录成功', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('testuser', 'correct-password'); await expect(page).toHaveURL('/dashboard'); await expect(page.getByText('欢迎回来')).toBeVisible(); }); test('错误密码显示错误提示', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('testuser', 'wrong-password'); await expect(loginPage.errorMessage).toContainText('用户名或密码错误'); }); });

断言策略是e2e测试里最容易出问题的地方。我的原则是:断言用户能感知到的结果,而不是实现细节。比如登录成功,断言URL跳转到了dashboard,断言页面上出现了欢迎文案,而不是去断言localStorage里有没有token。因为token的存储方式可能变,但用户看到的欢迎文案不会随便变。

另外,断言要尽量具体。toBeVisible()比toBeTruthy()好,toContainText('用户名或密码错误')比toContainText('错误')好。具体的断言在失败时能直接告诉你哪里不对,模糊的断言只会让你去猜。

5. e2e测试稳定性的治理经验

5.1 为什么你的e2e总是“偶发失败”

e2e测试最让人头疼的问题就是“偶发失败”——同一个用例,本地跑过了,CI上挂了;重跑一次又过了。这种不确定性比确定性失败更消耗团队信心。我总结下来,偶发失败的原因主要有四类:

第一类是等待时机不对。页面元素还没渲染出来就去点击,或者接口还没返回就去断言。Playwright虽然有自动等待,但有些场景它判断不了,比如一个动画过渡、一个懒加载图片、一个防抖后的接口调用。

第二类是测试数据污染。多个用例共用同一个账号,A用例改了密码,B用例就登录不上了。或者多个用例操作同一条订单,状态互相干扰。

第三类是环境不稳定。测试环境的数据库被其他人改了,第三方服务响应慢,CDN资源加载失败。这类问题最难排查,因为根因不在测试代码本身。

第四类是并行冲突。多个worker同时操作同一份数据,或者同时往同一个文件里写日志,导致互相覆盖。

5.2 等待策略:从sleep到智能等待

新手最容易犯的错误是用page.waitForTimeout(3000)这种硬等待。硬等待的问题是:等短了不够,等长了浪费时间,而且它不解决根本问题——你不知道到底在等什么。

正确的做法是等待具体的条件。Playwright提供了多种等待方式:

// 等待元素可见 await page.getByRole('button', { name: '提交' }).waitFor({ state: 'visible' }); // 等待接口返回 await page.waitForResponse(resp => resp.url().includes('/api/order') && resp.status() === 200 ); // 等待URL变化 await page.waitForURL('**/dashboard'); // 等待函数返回真值 await page.waitForFunction(() => document.querySelectorAll('.list-item').length > 0 );

这些等待方式比硬等待精确得多,而且失败时的报错信息更有指向性。如果waitForResponse超时了,你就知道是接口没返回;如果waitForFunction超时了,你就知道是列表没渲染出来。

注意:waitForResponse要放在触发请求的动作之前,否则可能请求已经返回了才开始监听,导致永远等不到。这个顺序问题我踩过好几次。

5.3 测试数据隔离的三种方案

数据隔离是保证e2e稳定性的另一个关键。我按推荐程度从高到低排列三种方案:

方案一:每个用例通过API创建独立数据。在测试开始前,调用后端API创建一个专属的测试账号、一条专属的订单。用例结束后,调用API清理数据。这种方案隔离性最好,但需要后端提供相应的测试接口。

方案二:使用数据库快照。在测试套件开始前,把数据库恢复到某个已知状态。所有用例在这个干净状态上运行。这种方案适合数据量不大、可以整体重置的场景。

方案三:数据命名空间隔离。给每个worker分配一个唯一前缀,所有测试数据都带这个前缀。比如worker-1创建的用户是user_w1_001,worker-2创建的是user_w2_001。这种方案不需要重置数据库,但需要业务代码支持按前缀查询和清理。

我通常优先选方案一,因为它最干净。如果后端暂时没有测试接口,就先用方案三过渡,同时推动后端补接口。

5.4 重试机制的正确用法

重试是一把双刃剑。用得好,它能帮你过滤掉真正的环境抖动;用不好,它会掩盖真实的bug。我的原则是:重试只用于处理环境层面的不确定性,不用于处理代码层面的确定性失败。

在Playwright里,retries配置在全局,但你可以针对单个用例覆盖:

test('这个用例允许重试两次', async ({ page }) => { test.info().annotations.push({ type: 'retry', description: '环境不稳定' }); // ... });

更重要的是,每次重试都应该有记录。如果某个用例在十次运行里重试了八次,那它就是一个“准失败”用例,需要去查根因,而不是靠重试蒙混过关。我会定期统计重试率,把重试率高的用例挑出来专门治理。

6. e2e与CI/CD的集成实战

6.1 在GitHub Actions中运行e2e

把e2e集成到CI里,核心要解决三个问题:环境准备、并行分片、结果报告。下面是一个GitHub Actions的配置示例:

name: E2E Tests on: pull_request: branches: [main] schedule: - cron: '0 2 * * *' jobs: e2e: runs-on: ubuntu-latest strategy: fail-fast: false matrix: shardIndex: [1, 2, 3, 4] shardTotal: [4] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx playwright install --with-deps chromium - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report-${{ matrix.shardIndex }} path: playwright-report/

这个配置把测试分成四个分片并行执行,每个分片跑四分之一用例。fail-fast: false保证一个分片失败不影响其他分片继续跑,这样你能一次性看到所有失败用例,而不是修一个跑一次。

6.2 测试环境的管理策略

e2e测试需要一个稳定的环境。我见过三种环境管理方式,各有优劣:

方式一:共享测试环境。所有开发者共用一套测试环境。优点是成本低,缺点是互相干扰严重,A的测试数据可能被B的测试删掉。

方式二:每个PR动态创建环境。每次提交PR时,自动创建一个独立的环境(比如用容器编排),PR合并后销毁。优点是隔离性好,缺点是成本高、创建慢。

方式三:本地容器化环境。用Docker Compose在本地或CI中启动一套完整的依赖服务。优点是可控性最强,缺点是需要维护容器配置。

我目前用的是方式三为主、方式一为辅。核心业务用例跑在容器化环境里,保证隔离性;一些非核心的兼容性用例跑在共享环境里,节省资源。

6.3 失败通知与报告解读

e2e失败之后,通知谁、怎么通知、通知里包含什么信息,这些细节决定了问题能不能被快速响应。我的做法是:

  • PR触发的冒烟e2e失败,直接在PR评论里@提交者,附上失败用例名和Trace链接。
  • 每日全量e2e失败,发到团队群,附上失败摘要和报告链接。
  • 连续三天失败的用例,自动创建一个issue,指派给对应模块的负责人。

报告方面,Allure报告比Playwright自带的HTML报告更适合团队协作,因为它支持按功能模块分组、支持添加附件(截图、视频、Trace)、支持历史趋势对比。配置Allure只需要在playwright.config.ts的reporter里加上allure-playwright,然后在CI里加一步生成报告的步骤。

7. 常见问题与排查技巧实录

7.1 选择器失效的排查思路

选择器失效是e2e维护中最常见的问题。排查步骤我通常这样走:

  1. 打开Playwright的Trace Viewer,找到失败的那一步。
  2. 看失败时的页面快照,确认元素是否真的存在。
  3. 如果元素存在但选择器匹配不到,检查是不是在iframe里、是不是在shadow DOM里、是不是被动态属性影响了。
  4. 如果元素不存在,检查是不是页面跳转到了错误页、是不是接口报错导致渲染中断。

避坑技巧:给关键元素加>// 文件上传 const fileChooserPromise = page.waitForEvent('filechooser'); await page.getByText('上传文件').click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles('fixtures/test-upload.pdf'); // 文件下载 const downloadPromise = page.waitForEvent('download'); await page.getByText('下载报告').click(); const download = await downloadPromise; await download.saveAs('downloads/report.pdf');

关键点是waitForEvent要放在触发动作之前,否则事件可能已经触发过了。下载的文件默认保存在临时目录,需要用saveAs显式保存到你指定的位置,否则测试结束后文件就被清理了。

7.4 常见问题速查表

问题现象可能原因排查方向解决方案
元素找不到选择器错误/页面未加载看Trace快照改用语义化选择器,加等待
点击无效元素被遮挡/未启用检查z-index和disabled属性等待可点击状态,滚动到可视区域
断言超时接口慢/渲染慢看网络请求时间线增加针对性等待,优化接口
偶发失败数据污染/并行冲突检查测试数据命名数据隔离,分片执行
本地过CI挂环境差异/时区/分辨率对比环境配置容器化统一环境
视频录制为空页面切换太快检查录制配置调整录制时机,加显式等待

这张表是我从实际排查记录里整理出来的,覆盖了八成以上的常见问题。遇到新问题时,我会先对照这张表,如果表里没有,再深入排查,然后把新问题补充进去。

8. 关于e2e profile1的一些观察

最近看到“e2e profile1”这个说法在社区里出现。我理解它可能指的是某种e2e测试的配置档案或者分层策略,比如把e2e测试按重要性分成profile1、profile2等不同级别。虽然我没有看到明确的定义,但从工程实践的角度,这种分层思路是合理的。

如果让我设计一套e2e分层方案,我会这样分:

Profile 1:核心冒烟。覆盖登录、下单、支付这三条最核心路径,每次提交都跑,五分钟内出结果。失败即阻断合并。

Profile 2:业务回归。覆盖所有主要业务功能,每日构建时跑,半小时内出结果。失败不阻断,但需要当天修复。

Profile 3:全量兼容。覆盖所有用例、所有浏览器、所有分辨率,发布前跑,两小时内出结果。失败需要评估是否阻断发布。

这种分层的好处是资源分配合理,快速反馈和全面验证兼顾。如果你正在设计e2e策略,可以参考这个思路,根据自己团队的业务特点调整分层标准和触发时机。

9. 我踩过的几个印象深刻的坑

说几个具体的故事,比抽象的建议更有参考价值。

第一个坑:用生产环境账号跑测试。早期做项目时,为了图方便,e2e测试直接连生产环境,用真实用户账号登录。结果有一次测试用例里的“删除订单”操作,把一个真实用户的订单删了。虽然最后通过客服补偿解决了,但这个教训让我从此坚持:e2e永远不连生产环境,永远用独立的测试数据和测试账号。

第二个坑:忽略时区问题。有一个用例断言订单创建时间是“今天”,本地跑没问题,CI上跑就失败。排查了半天才发现CI服务器的时区是UTC,而本地是东八区。后来所有时间相关的断言都改成用相对时间或者显式指定时区,不再依赖系统默认时区。

第三个坑:过度依赖重试。有一段时间,团队里有个用例重试率高达60%,但大家都觉得“反正重试能过,不管了”。直到有一次它连续重试五次都失败,才发现是一个接口在特定数据量下会超时。这个坑让我明白:重试率是技术债的晴雨表,必须定期治理。

第四个坑:测试用例之间互相依赖。早期写用例时,我让“下单”用例依赖“登录”用例先执行,因为下单需要登录状态。结果并行执行时,登录用例还没跑完,下单用例就开始了,直接失败。后来所有用例都改成自包含的——每个用例自己完成登录,不依赖其他用例的执行结果。

10. 一些实用的配置片段和技巧

最后分享几个我常用的配置片段,可以直接抄作业。

全局setup,用于登录状态复用:

// global-setup.ts import { chromium, FullConfig } from '@playwright/test'; async function globalSetup(config: FullConfig) { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto(`${config.use.baseURL}/login`); await page.getByLabel('用户名').fill(process.env.TEST_USER); await page.getByLabel('密码').fill(process.env.TEST_PASS); await page.getByRole('button', { name: '登录' }).click(); await page.waitForURL('**/dashboard'); await page.context().storageState({ path: 'auth.json' }); await browser.close(); } export default globalSetup;

然后在配置里引用storageState: 'auth.json',所有用例就自动带上登录状态,不用每个用例都重新登录。这一个优化能把测试套件的运行时间缩短30%以上。

自定义断言,用于等待接口完成:

async function waitForApi(page: Page, urlPattern: string) { return page.waitForResponse( resp => resp.url().includes(urlPattern) && resp.status() < 400, { timeout: 10000 } ); }

这个工具函数在需要等待接口返回后再断言的场景里特别有用,比硬等待可靠得多。

失败时自动附加截图到报告:

test.afterEach(async ({ page }, testInfo) => { if (testInfo.status !== testInfo.expectedStatus) { const screenshot = await page.screenshot(); await testInfo.attach('失败截图', { body: screenshot, contentType: 'image/png' }); } });

这个钩子保证每个失败用例都有一张现场截图,排查问题时不用再去翻视频。

e2e这件事,说到底是一个工程投入和收益的平衡。投入太少,线上问题频发;投入太多,维护成本压垮团队。找到那个平衡点,需要对自己团队的业务特点、技术栈、人员能力有清醒的认识。我上面写的这些,是我在几个不同规模团队里实践后沉淀下来的经验,不一定全对,但至少都是真金白银踩出来的。你可以根据自己的情况取用,也欢迎交流你的做法。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进