How I Used AI to Fix Our E2E Test Architecture

我接手的时候,E2E 测试的状态是这样的:

  • 2000 个测试用例
  • 通过率 60%
  • 运行时间 3 小时
  • 没有人相信测试结果

每次 CI 报告出来,开发者第一反应是"又是 flaky test",而不是"这个失败说明了什么"。

这已经不是一个技术问题,是一个信任问题。

我花了 8 周,用 AI agent 辅助,重建了整个 E2E 测试架构。一年后数据:

  • 通过率 94%
  • 运行时间 40 分钟(原来 3 小时)
  • Flaky rate 从 15% 降到 2%
  • 开发者愿意看测试报告了

这篇文章讲的是这个过程:不是怎么写测试,是怎么建一个让人愿意相信的测试架构

现状诊断:E2E 测试为什么失效

在动手之前,我花了 2 周做现状诊断。目标是搞清楚:为什么大家不信任 E2E 测试?

问题一:测试没有分层

现有测试结构是扁平的:2000 个测试用例,没有分层,没有优先级。

// 旧架构:所有测试一视同仁
describe('All Tests', function() {
    // 关键业务流程测试
    test('user can checkout', ...)
    test('payment processing', ...)

    // UI 细节测试
    test('button has correct color on hover', ...)
    test('modal has correct padding', ...)

    // 边界情况测试
    test('edge case: negative quantity', ...)
    test('edge case: very long email address', ...)

    // 混在一起,没有优先级
})

问题:关键业务流程的测试和 UI 细节测试消耗同样的 CI 时间,出问题时开发者不知道要先看哪个

问题二:测试之间强耦合

测试之间通过全局状态耦合,一个测试会影响下一个测试的结果。

// 耦合测试示例
test('user login', async () => {
    await page.fill('[name="email"]', 'test@example.com')
    await page.fill('[name="password"]', 'password123')
    await page.click('[type="submit"]')

    // 把登录状态存在全局变量里
    global.loggedInUser = await getCurrentUser()
})

test('user can see dashboard', async () => {
    // 依赖上一个测试的全局状态
    // 如果上一个测试失败,这个也会失败
    expect(global.loggedInUser.dashboardUrl).toBeTruthy()
})

test('user can place order', async () => {
    // 依赖前两个测试都成功
    await dashboardPage.navigateTo(global.loggedInUser.dashboardUrl)
    // ...
})

问题:一个测试失败会导致后续测试级联失败,但失败原因是第一个测试,不是后续测试本身

问题三:没有稳定的测试数据管理

测试数据通过 API 直接操作,每个测试自己创建、自己清理。

# 混乱的测试数据管理
def test_create_order():
    # 创建用户
    user = api.create_user(email="test1@example.com")
    # 创建产品
    product = api.create_product(name="Test Product")
    # 创建订单
    order = api.create_order(user_id=user.id, product_id=product.id)

    # 清理:删除用户(但产品还在,订单记录也在)
    api.delete_user(user.id)
    # 下次运行可能遇到 email 重复、product 名称重复等问题

def test_create_order_similar():
    # 又创建一个同名产品,可能失败
    product = api.create_product(name="Test Product")  # 名称冲突
    # ...

问题:测试之间争抢数据资源,数据清理不完整导致测试结果不稳定

问题四:等待时间随机

测试里的等待时间是拍脑袋定的。

// 拍脑袋的等待
test('user login', async () => {
    await page.fill('[name="email"]', 'test@example.com')
    await page.click('[type="submit"]')

    // 等待 3 秒,为什么是 3 秒?因为上次跑的时候等了 3 秒
    await page.waitForTimeout(3000)

    // 有时候 API 慢,需要 5 秒,这个测试就失败了
    // 有时候网络快,1 秒就够了,等 3 秒浪费时间
})

问题:固定等待时间无法适应不同的运行环境,慢环境失败,快环境浪费时间

架构重建:四层测试模型

诊断做完,我设计的新的测试架构是四层模型:

┌─────────────────────────────────────┐
│ Layer 4: Critical Path (smoke tests)│ ← CI 的 Gate,必须通过
├─────────────────────────────────────┤
│ Layer 3: Integration Flows          │ ← 核心业务流程
├─────────────────────────────────────┤
│ Layer 2: Feature Coverage          │ ← 单元测试覆盖不到的部分
├─────────────────────────────────────┤
│ Layer 1: UI Components             │ ← 可选运行,低优先级
└─────────────────────────────────────┘

Layer 1: UI Components(UI 组件测试)

import pytest
from playwright.sync_api import Page, expect

class ButtonComponent:
    """按钮组件测试"""

    def test_button_renders_correctly(self, page: Page):
        """按钮正确渲染"""
        page.goto("/components/button")
        button = page.locator('[data-testid="primary-button"]')
        expect(button).to_be_visible()
        expect(button).to_have_text("Submit")

    def test_button_hover_state(self, page: Page):
        """按钮悬停状态"""
        page.goto("/components/button")
        button = page.locator('[data-testid="primary-button"]')

        button.hover()
        # 不测具体颜色(容易碎),测是否有状态变化
        expect(button).to_have_attribute("data-hovered", "true")

    def test_button_disabled_state(self, page: Page):
        """禁用状态"""
        page.goto("/components/button")
        button = page.locator('[data-testid="primary-button"]')
        button.set_disabled()

        expect(button).to_be_disabled()
        # 点击不触发任何事件
        button.click()
        # 没有导航发生

这层测试的价值是记录 UI 组件的行为,不是验证样式。测试名称描述行为,而不是样式。

Layer 2: Feature Coverage(功能覆盖测试)

def test_shopping_cart_add_item(page: Page, test_user):
    """购物车添加商品"""
    page.goto("/shop")

    # 搜索商品
    page.fill('[data-testid="search-input"]', test_user.search_term)
    page.click('[data-testid="search-button"]')

    # 添加到购物车
    first_product = page.locator('[data-testid="product-card"]').first
    first_product.locator('[data-testid="add-to-cart"]').click()

    # 验证购物车更新
    cart_badge = page.locator('[data-testid="cart-badge"]')
    expect(cart_badge).to_have_text("1")

    # 验证侧边栏购物车显示
    cart_sidebar = page.locator('[data-testid="cart-sidebar"]')
    expect(cart_sidebar).to_be_visible()
    expect(cart_sidebar.locator('.cart-item')).to_have_count(1)

这层测试关注功能是否按预期工作,使用 Page Object Pattern 减少 UI 变化的影响。

Layer 3: Integration Flows(集成流程测试)

@ pytest.fixture
def authenticated_browser(page: Page, test_user):
    """预先认证的浏览器"""
    # 登录,获取持久化 session
    page.goto("/login")
    page.fill('[name="email"]', test_user.email)
    page.fill('[name="password"]', test_user.password)
    page.click('[type="submit"]')
    page.wait_for_url("**/dashboard")

    # 保存认证状态
    storage = page.context.storage_state()

    yield page

    # 清理(保留 fixture 作用域内的状态)

@pytest.mark.integration
class TestCheckoutFlow:
    """结账流程集成测试"""

    def test_complete_checkout_flow(self, authenticated_browser: Page, test_cart_with_items):
        """完整结账流程"""
        page = authenticated_browser

        # 1. 进入购物车
        page.goto("/cart")
        cart_items = page.locator('[data-testid="cart-item"]')
        expect(cart_items).to_have_count(len(test_cart_with_items))

        # 2. 进入结账页面
        page.click('[data-testid="checkout-button"]')
        page.wait_for_url("**/checkout")

        # 3. 填写地址
        page.fill('[data-testid="address-line1"]', test_cart_with_items.address)
        page.fill('[data-testid="postal-code"]', test_cart_with_items.postal_code)
        page.click('[data-testid="continue-to-payment"]')

        # 4. 选择支付方式
        page.click('[data-testid="payment-method-card"]')

        # 5. 确认订单
        page.click('[data-testid="place-order"]')

        # 6. 验证成功
        expect(page.locator('[data-testid="order-confirmation"]')).to_be_visible()
        order_id = page.locator('[data-testid="order-id"]').text_content()
        expect(order_id).to_match(r'^ORD-\d+$')

        # 7. 验证邮件发送
        test_email_service.assert_email_sent(
            to=test_cart_with_items.email,
            subject="Order Confirmation"
        )

集成测试使用 fixture 管理依赖,确保测试之间的隔离。

Layer 4: Critical Path(关键路径测试,Smoke Tests)

@pytest.mark.critical
class TestCriticalPath:
    """关键路径测试 - 必须全部通过"""

    @pytest.mark.critical
    def test_user_can_signup_and_login(self, page: Page):
        """用户注册和登录"""
        # 关键路径 1: 注册 -> 登录 -> 登出 能跑通
        pass

    @pytest.mark.critical
    def test_core_search_flow(self, page: Page):
        """核心搜索流程"""
        # 关键路径 2: 搜索 -> 查看结果 -> 查看详情 能跑通
        pass

    @pytest.mark.critical
    def test_basic_purchase_flow(self, page: Page):
        """基本购买流程"""
        # 关键路径 3: 选商品 -> 加购物车 -> 结账 -> 支付 -> 确认 能跑通
        pass

    @pytest.mark.critical
    def test_critical_backend_health(self):
        """关键后端服务健康"""
        # 非 UI 测试:验证关键 API 可用
        pass

这层是 CI 的 gate,只有这层通过,才会执行后续 Layer

解决测试数据问题:Seed Data Pattern

测试数据混乱是导致 flaky 的主要原因之一。我设计了 Seed Data Pattern:

# test/data/seed_data.py
class SeedDataManager:
    """
    统一的测试数据管理
    每个测试使用独立的 seed data,测试结束后清理
    """

    def __init__(self, db_connection):
        self.db = db_connection
        self.created_records = []

    def create_user(self, **overrides):
        """创建测试用户"""
        defaults = {
            "email": f"test_{uuid4().hex[:8]}@example.com",  # 唯一 email
            "name": "Test User",
            "tier": "standard"
        }
        user_data = {**defaults, **overrides}

        user = self.db.users.create(**user_data)
        self.created_records.append(("users", user.id))
        return user

    def create_product(self, **overrides):
        """创建测试产品"""
        defaults = {
            "sku": f"SKU-{uuid4().hex[:8]}",  # 唯一 SKU
            "name": "Test Product",
            "price": 99.99,
            "stock": 100
        }
        product_data = {**defaults, **overrides}

        product = self.db.products.create(**product_data)
        self.created_records.append(("products", product.id))
        return product

    def create_cart_with_items(self, user, item_count=3):
        """创建带商品的购物车"""
        cart = self.db.carts.create(user_id=user.id)
        products = [self.create_product() for _ in range(item_count)]

        for product in products:
            self.db.cart_items.create(cart_id=cart.id, product_id=product.id)

        self.created_records.append(("carts", cart.id))
        return cart

    def cleanup(self):
        """清理所有创建的记录"""
        for table, record_id in reversed(self.created_records):
            self.db[table].delete(record_id)
        self.created_records = []

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.cleanup()

使用方式:

def test_shopping_cart_with_fixture(db):
    """使用 seed data fixture"""
    with SeedDataManager(db) as seed:
        # 所有创建的数据会在测试结束后自动清理
        user = seed.create_user(email="cart_test@example.com")
        cart = seed.create_cart_with_items(user, item_count=5)

        # 测试逻辑...
        # 即使测试失败,cleanup() 仍会在 __exit__ 中执行

解决等待时间问题:Smart Wait Utilities

固定等待时间是 flaky 的另一个主要原因。改用智能等待:

# test/utils/smart_wait.py
from playwright.sync_api import Page, TimeoutError

class SmartWait:
    """智能等待工具"""

    @staticmethod
    def for_element_visible(page: Page, selector: str, timeout: int = 10000):
        """等待元素可见,自动处理加载状态"""
        try:
            element = page.locator(selector)
            element.wait_for(state="visible", timeout=timeout)
            return element
        except TimeoutError:
            # 如果超时,打印页面状态帮助调试
            print(f"Timeout waiting for {selector}")
            print(f"Page URL: {page.url}")
            print(f"Page title: {page.title()}")
            print(f"Visible elements: {page.locator(':visible').count()}")
            raise

    @staticmethod
    def for_network_idle(page: Page, timeout: int = 30000):
        """等待网络空闲"""
        page.wait_for_load_state("networkidle", timeout=timeout)

    @staticmethod
    def for_api_response(page: Page, url_pattern: str, timeout: int = 10000):
        """等待特定 API 响应"""
        def check_response(response):
            return response.url.startswith(url_pattern) and response.status < 400

        with page.expect_response(check_response, timeout=timeout) as response_info:
            # 触发 API 请求
            yield response_info.value

使用方式:

def test_async_operation_complete(page: Page):
    """测试异步操作完成"""
    # 触发异步操作
    page.click('[data-testid="start-export"]')

    # 使用智能等待
    with SmartWait.for_api_response(page, "/api/export/status"):
        page.click('[data-testid="check-status"]')

    # 等待结果元素出现
    SmartWait.for_element_visible(
        page,
        '[data-testid="export-complete"]',
        timeout=30000
    )

AI Agent 的辅助角色

整个重建过程中,AI agent 帮我做了几件事:

1. 测试翻译:Selenium → Playwright

旧系统用 Selenium,新系统用 Playwright。AI agent 帮我做了代码翻译。

# AI 生成的 Playwright 版本(从 Selenium 翻译)
# 旧版 Selenium
driver.find_element(By.CSS_SELECTOR, ".product-card .add-btn").click()

# AI 生成的 Playwright 版
page.locator('.product-card .add-btn').click()
# 或者更稳定的方式
page.get_by_test_id("add-to-cart").click()

AI 的建议是把 CSS selector 换成 data-testid,提高稳定性。这个建议是对的。

2. 测试用例生成

给定一个 API endpoint,AI agent 能生成基础测试用例:

# AI 生成的测试用例模板
@pytest.mark.parametrize("input_data,expected", [
    ({"quantity": 1}, "success"),
    ({"quantity": 0}, "validation_error"),
    ({"quantity": -1}, "validation_error"),
    ({"quantity": 999999}, "stock_error"),
])
def test_add_to_cart_quantity_boundaries(
    page: Page,
    authenticated_user,
    input_data,
    expected
):
    """边界测试:购物车数量"""
    page.goto(f"/product/{PRODUCT_ID}")

    quantity_input = page.get_by_test_id("quantity-input")
    quantity_input.fill(str(input_data["quantity"]))

    page.get_by_test_id("add-to-cart").click()

    if expected == "success":
        expect(page.get_by_test_id("cart-badge")).to_have_text("1")
    elif expected == "validation_error":
        expect(page.get_by_test_id("error-message")).to_be_visible()
    elif expected == "stock_error":
        expect(page.get_by_test_id("stock-error")).to_be_visible()

AI 生成的不是完美的测试,但是好的起点。我 review 后调整边界值。

3. Flaky Test 分析

最难的部分:分析为什么测试 flaky。AI agent 分析日志,找出模式:

# AI 分析结果
def analyze_flaky_tests(flaky_test_logs: list) -> dict:
    """
    分析 flaky 测试的原因模式
    """

    patterns = {
        "timeout": 0,
        "selector_not_found": 0,
        "network_error": 0,
        "data_conflict": 0,
        "state_leak": 0,
    }

    for log in flaky_test_logs:
        if "Timeout" in log:
            patterns["timeout"] += 1
        if "selector" in log.lower():
            patterns["selector_not_found"] += 1
        if "Network" in log:
            patterns["network_error"] += 1
        if "unique constraint" in log.lower():
            patterns["data_conflict"] += 1
        if "previous test" in log.lower():
            patterns["state_leak"] += 1

    return patterns

# 分析结果:
# {'timeout': 45, 'selector_not_found': 30, 'network_error': 15,
#  'data_conflict': 8, 'state_leak': 2}

根据分析结果,我确定了优先级:
1. Timeout 问题 → 改用智能等待
2. Selector 问题 → 统一 data-testid
3. Network 问题 → 增加重试机制
4. Data conflict → 修复 seed data 管理

一年后的数据

一年后,这套架构的数据:

指标 重建前 重建后 变化
通过率 60% 94% +34%
运行时间 3 小时 40 分钟 -77%
Flaky rate 15% 2% -13%
测试覆盖(关键路径) N/A 100% -
平均修复时间 2 小时 20 分钟 -83%

关键路径测试(Layer 4)在 CI 中 gate 住了所有 deploy,关键业务流程的测试覆盖达到了 100%。

踩坑记录

重建过程中踩的几个坑:

坑 1:Layer 1 测试过多

一开始 Layer 1 有 1500 个 UI 组件测试,占了运行时间的 40%,但对质量保障的价值很低。

解决:把 Layer 1 改成可选运行,默认不跑。真正需要回归 UI 组件时才运行。

坑 2:Seed data 的并发问题

CI 并发运行时,seed data 的 email 依然可能冲突(用了时间戳做区分)。

解决:在 email 里加了 UUID 后缀,确保全局唯一:

email = f"{base_email}_{uuid4().hex[:8]}@example.com"

坑 3:过度依赖 AI 生成的测试

AI 生成的边界测试,有时候边界值是错的。

解决:边界值需要人工 review,不能直接用 AI 的输出。AI 是起点,不是终点。

核心经验

8 周重建,1 年运行,我的核心经验:

1. 技术问题往往是组织问题的症状

E2E 测试 flaky率高、没人信任,这不是技术问题,这是团队对测试优先级理解不一致的症状。重建架构之前,先让团队达成共识:测试是用来干嘛的?

2. 通过率不是目标,稳定才是

一开始团队追求 100% 通过率,导致大量"修复"是删除测试,而不是修复问题。稳定比通过率重要。

3. AI 是加速器,不是替代品

AI 能帮你写测试,但不能替你思考测试策略。架构设计、优先级判断、数据管理策略,这些还是要人来做。

4. 测试的分层和隔离是信任的基础

没有分层,所有测试混在一起,失败时不知道该先看哪个。有了分层和隔离,开发者才愿意看测试报告。


这套架构在 GitHub 上(链接略),有需要的自取,欢迎提 Issue。