Playwright UI与API集成自动化测试:构建全链路闭环测试框架

📅 2026/7/6 9:46:14
Playwright UI与API集成自动化测试:构建全链路闭环测试框架
1. 项目概述为什么我们需要一个“全栈”自动化模板如果你是一名测试工程师或者正在学习自动化测试大概率听过 Playwright 这个名字。它早已不是那个“新出的浏览器自动化工具”而是成为了许多团队在 UI 自动化测试上的首选。但今天我想聊的远不止是“用 Playwright 点点页面”这么简单。我们经常面临一个困境UI 自动化用例写了一大堆但一旦后端接口逻辑变动前端元素也跟着变维护成本陡增而纯接口自动化虽然稳定快速却又无法覆盖真实的用户交互和前端渲染逻辑。有没有一种方法能把两者的优势结合起来形成一个既稳定又贴近用户真实操作、还能高效回归的自动化体系这就是“Playwright UI与API接口自动化模板”项目要解决的核心问题。它不是一个简单的脚本集合而是一套设计思路和工程实践旨在利用 Playwright 同时驾驭浏览器和网络请求的能力构建“从用户界面操作触发到后端接口数据验证”的完整闭环测试。简单说它让我们的自动化脚本既能“看到”用户看到的页面也能“听到”页面背后发生的所有网络对话从而实现更立体、更健壮的自动化覆盖。对于需要频繁回归的 Web 应用、特别是带有复杂交互和动态数据的前后端分离项目这套模板能显著提升测试的深度和效率把测试人员从重复的“点点点”和“查日志”中解放出来。2. 核心设计思路UI为表API为里构建自动化双核引擎2.1 从“单点验证”到“链路透视”的思维转变传统的 UI 自动化验证点往往停留在页面元素是否存在、文本是否正确。这就像只检查商店的橱窗是否干净却不关心仓库里的库存是否准确。而纯接口自动化则像是只清点仓库库存却不关心商品摆上橱窗后顾客能否顺利找到并购买。我们的模板设计核心是促成一次思维转变将一次用户操作UI事件及其触发的所有后端交互API调用视为一个不可分割的测试单元。在这个单元里Playwright 扮演了两个角色交互执行者模拟真实用户进行点击、输入、导航等操作。网络监听者拦截并分析此次操作所引发的所有网络请求和响应。基于此我们的自动化用例就可以实现诸如“点击提交订单按钮不仅检查页面是否跳转到成功页还要验证是否向/api/order/create发送了正确的请求体并且返回的订单ID能成功在页面上展示出来”。这种“UI操作触发API数据校验UI结果确认”的链路极大地增强了测试的断言强度和场景真实性。2.2 模板的四大核心模块设计为了实现上述思路模板在结构上需要清晰划分职责。我建议的目录结构如下playwright-automation-template/ ├── core/ # 核心框架层 │ ├── base_page.py # 页面对象基类封装通用操作 │ ├── api_client.py # 接口请求客户端也可复用Playwright的API能力 │ └── request_interceptor.py # 网络请求监听与拦截器核心 ├── pages/ # 页面对象模型POM │ ├── login_page.py │ ├── home_page.py │ └── order_page.py ├── tests/ # 测试用例层 │ ├── ui_flows/ # 纯UI流程测试 │ ├── api_flows/ # 纯API流程测试 │ └── integrated/ # UI与API集成测试模板重点 │ └── test_order_creation.py ├── fixtures/ # 测试夹具如测试数据、上下文 ├── utils/ # 工具函数数据生成、断言增强等 ├── playwright.config.js # Playwright主配置 └── requirements.txt # Python依赖模块解析request_interceptor.py这是模板的“神经中枢”。它利用 Playwright 的page.on(request)和page.on(response)事件对特定的 URL 模式进行监听。我们可以在这里捕获请求参数、响应状态码和响应体并将其存储到测试上下文中供后续断言使用。关键在于设计一个灵活的匹配和存储机制避免监听过多请求导致性能下降或数据混乱。base_page.py所有页面对象的父类。除了封装常见的点击、输入方法外关键是要集成请求监听器。例如可以提供一个wait_for_response(url_pattern, action_callback)方法先启动对某个接口的监听然后执行传入的点击操作action_callback最后返回捕获到的响应。这样就把监听和操作优雅地结合在了一起。integrated/测试目录这里存放的测试用例是模板价值的集中体现。一个典型的用例会混合调用页面对象的方法触发UI操作和直接对捕获的API响应进行断言。2.3 技术选型考量为什么是Playwright而非Selenium或Cypress市面上优秀的自动化工具不少选择 Playwright 作为这套模板的基础是基于以下几个关键优势的考量原生支持多上下文与请求拦截Playwright 的 API 设计对网络控制非常友好。page.route()可以轻松修改请求或响应page.on(request/response)监听事件清晰易用。相比之下在 Selenium 中实现类似功能需要依赖浏览器开发者工具协议CDP配置更繁琐。Cypress 虽然也能拦截但其运行机制和架构与 Playwright 有较大不同。自动等待与稳定性Playwright 内置的自动等待机制等待元素可操作、网络空闲等极大地提高了 UI 自动化脚本的稳定性。在集成测试中UI 的稳定性直接关系到能否可靠地触发目标 API这一点至关重要。跨浏览器与移动端模拟一致性一套脚本无需修改即可在 Chromium、Firefox、WebKit 上运行并且能非常逼真地模拟移动设备如 iPhone 13。这对于需要验证不同终端交互一致性的场景非常有用。强大的录制与调试工具playwright codegen命令可以快速生成操作代码为构建页面对象和编写用例提供了极佳的起点。playwright inspector则让调试变得直观。实操心得在早期探索时我也尝试过用Selenium Wire或pytest-httpserver等库在 Selenium 框架下做请求拦截但总感觉像是打补丁链路不够顺畅。Playwright 将浏览器自动化与网络层控制深度集成提供了“开箱即用”的一体化解决方案这让构建我们想要的集成模板在技术实现上更加优雅和直接。3. 核心实现细节打造可复用的请求监听与断言机制3.1 实现一个健壮的请求拦截器拦截器的目标是在测试执行过程中按需捕获指定的 API 调用并将其请求和响应数据安全地保存下来供测试断言使用。以下是一个 Python 版本的简化实现核心# core/request_interceptor.py import json from typing import Dict, Any, Optional, Callable from playwright.sync_api import Page, Response, Request class RequestInterceptor: def __init__(self, page: Page): self.page page self._captured_responses: Dict[str, Dict[str, Any]] {} # 存储捕获的响应 self._response_listeners: Dict[str, Callable] {} # 注册监听回调 def start_listening(self): 开始监听页面请求和响应 self.page.on(response, self._handle_response) def _handle_response(self, response: Response): 处理响应事件 url response.url # 检查是否有监听器匹配这个URL for pattern, callback in self._response_listeners.items(): if pattern in url: try: # 异步响应体需要特殊处理这里以同步API为例 # 注意response.body() 在同步API中可直接调用但可能需判断状态 if response.ok: body response.body() json_data json.loads(body) if application/json in response.headers.get(content-type, ) else None captured_data { url: url, status: response.status, headers: dict(response.headers), body: json_data or body.decode(utf-8) if body else None } self._captured_responses[url] captured_data # 如果有注册的回调执行它 if callback: callback(captured_data) except Exception as e: print(fError processing response from {url}: {e}) def wait_for_response(self, url_pattern: str, action_callback: Callable, timeout: float 30000) - Optional[Dict[str, Any]]: 等待特定URL模式的响应。 :param url_pattern: URL中包含的字符串模式 :param action_callback: 触发该响应的UI操作函数如 page.click() :param timeout: 等待超时时间毫秒 :return: 捕获的响应数据或None from playwright.sync_api import TimeoutError as PlaywrightTimeoutError import time start_time time.time() # 先执行触发操作 action_callback() # 然后轮询检查是否捕获到目标响应 while (time.time() - start_time) * 1000 timeout: for url, data in self._captured_responses.items(): if url_pattern in url: # 找到后可以选择性移除避免影响后续断言 # captured_data self._captured_responses.pop(url) return data time.sleep(0.1) # 短暂休眠避免CPU空转 raise PlaywrightTimeoutError(fTimeout while waiting for response matching pattern: {url_pattern}) def get_captured_response(self, url_pattern: str) - Optional[Dict[str, Any]]: 获取已捕获的特定响应 for url, data in self._captured_responses.items(): if url_pattern in url: return data return None def clear_captured_data(self): 清除所有已捕获的数据 self._captured_responses.clear()关键点解析wait_for_response方法这是集成测试的“粘合剂”。它接受一个action_callback例如一个点击函数在执行这个回调前后逻辑上已经通过start_listening在后台监听着网络。方法内部通过轮询检查是否捕获到了目标响应。这种模式将“触发动作”和“等待结果”封装成了一个原子操作用例编写起来非常直观。响应体处理注意response.body()的调用。在 Playwright 的同步 API 中这通常是可行的但务必检查响应状态和内容类型。对于大型响应体要考虑性能。我们这里优先处理 JSON 响应因为这是 API 断言中最常见的格式。数据存储与清理使用字典存储捕获的响应键可以是完整 URL 或自定义标识。在setUp/tearDown或pytest.fixture中务必调用clear_captured_data来防止测试用例间的数据污染。3.2 增强型页面对象基类集成拦截能力有了拦截器我们需要将其能力注入到页面对象中让页面操作能自动关联网络监听。# core/base_page.py from playwright.sync_api import Page from .request_interceptor import RequestInterceptor class BasePage: def __init__(self, page: Page): self.page page self.interceptor RequestInterceptor(page) self.interceptor.start_listening() # 初始化即开始监听 def navigate(self, url: str): self.page.goto(url) # 可以在这里添加通用的等待逻辑如等待某个关键元素 def click_and_wait_for_response(self, selector: str, url_pattern: str, **kwargs): 点击元素并等待特定的API响应。 :param selector: 元素选择器 :param url_pattern: 期望响应的URL包含的字符串 :return: 捕获的响应数据 def click_action(): self.page.click(selector, **kwargs) return self.interceptor.wait_for_response(url_pattern, click_action) def fill_and_wait_for_response(self, selector: str, value: str, url_pattern: str, **kwargs): 填充输入框并等待特定API响应如搜索建议 def fill_action(): self.page.fill(selector, value, **kwargs) return self.interceptor.wait_for_response(url_pattern, fill_action)设计意图BasePage提供了click_and_wait_for_response这类集成方法。这样在具体的页面对象如LoginPage里一个登录操作就可以同时完成点击和登录接口的验证无需在测试用例中编写冗长的监听和等待代码。3.3 编写一个UI与API融合的测试用例让我们看一个电商场景下的完整示例测试用户添加商品到购物车。# tests/integrated/test_cart_operations.py import pytest from pages.login_page import LoginPage from pages.product_page import ProductPage from pages.cart_page import CartPage class TestIntegratedCart: pytest.fixture(autouseTrue) def setup(self, page): # page fixture 由pytest-playwright提供 self.page page self.login_page LoginPage(page) self.product_page ProductPage(page) self.cart_page CartPage(page) # 假设已有登录态或先执行登录 self.login_page.login(test_user, password123) def test_add_item_to_cart_and_verify_api(self): 测试在商品页点击“加入购物车”验证UI变化并验证后端添加购物车接口的调用。 # 1. 导航到商品页 self.product_page.navigate_to_product(product-123) # 2. 执行核心操作点击加入购物车并等待对应的API调用 # 假设加入购物车会调用 /api/cart/add 接口 add_to_cart_button_selector button[data-testidadd-to-cart] api_response self.product_page.click_and_wait_for_response( selectoradd_to_cart_button_selector, url_pattern/api/cart/add ) # 3. 对捕获的API响应进行断言 assert api_response is not None, 未捕获到加入购物车的API响应 assert api_response[status] 200, fAPI调用失败状态码{api_response[status]} response_body api_response[body] # 假设成功响应体为 {success: true, cartItemId: item_001} assert response_body[success] is True assert cartItemId in response_body captured_item_id response_body[cartItemId] # 4. 验证UI上的即时反馈如按钮文字变为“已加入” added_text self.product_page.get_add_to_cart_button_text() assert 已加入 in added_text # 5. 导航到购物车页面验证商品确实存在 self.cart_page.navigate() cart_items self.cart_page.get_cart_item_ids() assert captured_item_id in cart_items, f购物车中未找到ID为 {captured_item_id} 的商品 # 6. 可选甚至可以再次监听购物车列表接口验证数据一致性 # 例如点击刷新购物车列表验证返回的数据中包含刚添加的商品ID用例逻辑拆解触发与监听通过click_and_wait_for_response将 UI 操作点击与 API 监听/api/cart/add绑定。这是模板的核心。API层断言直接对接口的响应状态码和业务数据进行断言如success: true这比只通过 UI 判断更早、更稳定地发现后端逻辑问题。UI层断言验证操作后前端的即时视觉反馈确保用户体验正确。状态一致性断言导航到相关页面购物车验证操作产生的最终状态在 UI 上得以正确呈现。这里甚至可以再次利用拦截器去验证购物车列表接口返回的数据是否包含新添加的商品实现“UI操作 - API确认 - 另一UI状态 - 另一API确认”的深度校验链。注意事项这种深度集成测试的用例其执行时间会比纯 UI 或纯 API 测试更长。因此它更适合用于核心业务流如下单、支付的回归测试而不是全量测试。需要将其放入冒烟测试套件或核心回归套件中并合理安排执行频率。4. 进阶技巧处理动态数据、认证与异步流程4.1 动态参数的处理与关联在实际场景中API 请求常常包含动态参数如token、timestamp或上一步生成的orderId。我们的模板需要能灵活处理这种关联。技巧一从响应中提取数据用于后续请求或断言。在request_interceptor.py的_handle_response方法中我们捕获了响应体。可以在测试用例中将这些数据存入一个“测试上下文”字典或 fixture 中。# 在测试用例中 def test_order_flow(self): # ... 登录等前置步骤 ... # 1. 创建订单捕获响应中的订单号 create_order_response self.order_page.submit_order_and_wait_for_api(/api/order/create) order_id create_order_response[body][orderId] # 将订单号存入页面对象或测试类的属性中供后续使用 self.order_page.order_context[current_order_id] order_id # 2. 支付订单请求体中需要使用上一步的订单号 # pay_order 方法内部可以从 context 中读取 order_id 并构造请求 pay_response self.order_page.pay_order_and_wait_for_api(/api/payment/process) assert pay_response[status] 200技巧二使用page.route()修改请求。有时我们需要在请求发出前修改其参数或者模拟特定的请求/响应。Playwright 的page.route()非常强大。# 在页面对象或测试设置中 def setup_mock(self): # 拦截所有对 /api/user/profile 的请求并返回一个模拟响应 self.page.route(**/api/user/profile, lambda route: route.fulfill( status200, content_typeapplication/json, bodyjson.dumps({username: mock_user, level: VIP}) )) # 或者修改特定请求的请求体 self.page.route(**/api/cart/update, lambda route: route.continue_(post_datamodified_data))4.2 处理认证状态如Cookie、TokenUI 自动化通常依赖浏览器上下文来保持登录态Cookie。而如果你想在同一个测试中穿插一些纯 API 调用比如用requests库直接调接口清理测试数据就需要共享认证信息。解决方案从 Playwright 上下文中提取 Cookie用于 API 客户端。# core/api_client.py import requests from playwright.sync_api import BrowserContext class AuthenticatedAPIClient: def __init__(self, context: BrowserContext, base_url: str): self.session requests.Session() self.base_url base_url # 从BrowserContext中获取Cookie并设置到requests session中 cookies context.cookies() for cookie in cookies: # 注意可能需要处理域名、路径等属性这里做简化 self.session.cookies.set(cookie[name], cookie[value]) def post(self, endpoint, **kwargs): url f{self.base_url}{endpoint} return self.session.post(url, **kwargs) # 类似地实现 get, put, delete 等方法在测试中你可以同时拥有页面对象和这个认证的 API 客户端实现 UI 操作和 API 操作的灵活混合。4.3 应对复杂的异步交互与等待现代前端应用大量使用异步加载和动态渲染。一个按钮点击可能触发多个 API 调用且 UI 更新是渐进的。策略使用 Playwright 更精细的等待策略结合网络监听。def test_complex_async_ui(self): # 监听一个关键的初始数据加载接口 initial_data_response self.dashboard_page.wait_for_response( url_pattern/api/initial-data, action_callbacklambda: self.dashboard_page.navigate() # 导航动作触发加载 ) # 断言初始数据 assert initial_data_response[body][loaded] is True # 执行一个操作可能触发多个后续API with self.page.expect_response(**/api/update/**) as update_info: with self.page.expect_response(**/api/refresh/**) as refresh_info: self.dashboard_page.click_refresh_button() update_response update_info.value refresh_response refresh_info.value # 分别对两个响应进行断言 # 最后等待UI上的某个特定元素出现表示所有异步更新完成 self.page.wait_for_selector(.data-update-complete-indicator)page.expect_response()是一个非常好的方法它返回一个事件句柄允许你精确地等待一个或多个特定的响应完成然后再进行下一步操作或断言。5. 工程化实践配置、报告与持续集成5.1 灵活的Playwright配置playwright.config.js(或.ts) 是控制测试行为的核心。对于集成测试模板以下配置项尤为重要// playwright.config.js const { defineConfig, devices } require(playwright/test); module.exports defineConfig({ testDir: ./tests, // 测试目录 fullyParallel: false, // 集成测试建议设为false因为可能有测试顺序依赖 forbidOnly: !!process.env.CI, // CI环境下禁止使用test.only retries: process.env.CI ? 2 : 0, // CI下重试2次 workers: 1, // 集成测试建议单worker运行避免状态干扰 reporter: [ [html, { outputFolder: playwright-report, open: never }], [junit, { outputFile: test-results/results.xml }], // 用于CI集成 [list] // 控制台输出 ], use: { baseURL: process.env.BASE_URL || http://localhost:3000, headless: true, // CI环境通常为true viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true, screenshot: only-on-failure, video: retain-on-failure, trace: retain-on-failure, // 故障追踪非常有用 }, projects: [ { name: chromium, use: { ...devices[Desktop Chrome] }, }, // 可以定义其他项目如移动端测试或API专用测试 { name: api-integration, // 一个专门运行集成测试的项目 testMatch: **/integrated/**, // 只运行integrated目录下的测试 use: { ...devices[Desktop Chrome], // 可以为集成测试项目配置更高的超时时间 actionTimeout: 30000, navigationTimeout: 30000, }, }, ], });关键配置解析workers: 1对于有状态、有顺序依赖的集成测试建议串行执行workers: 1避免并行执行导致共享资源如测试账号、数据冲突。trace: retain-on-failure当测试失败时会保存一个追踪文件。通过playwright show-trace命令打开可以可视化地回顾测试执行的每一步包括网络请求、DOM 快照等是调试复杂集成失败的利器。多项目配置通过projects可以为不同类型的测试如纯 UI、集成、移动端定义不同的配置用testMatch来筛选用例非常灵活。5.2 生成有价值的测试报告清晰的报告能快速定位问题。Playwright 自带的 HTML 报告已经非常强大但我们可以为其注入更多 API 层面的信息。自定义 Reporter 来包含 API 断言结果你可以编写一个简单的自定义 reporter将我们在拦截器中捕获的 API 请求/响应信息特别是断言失败时的详情添加到测试报告中。这通常需要 hook 到测试的生命周期中在测试失败时将额外信息如失败的 API URL、期望与实际响应体附加到测试结果里。虽然有一定复杂度但对于团队协作和问题排查价值巨大。一个更简单的实践是在测试用例中当 API 断言失败时使用print或logging输出详细的上下文信息这些信息会出现在测试运行的标准输出中同样有助于排查。5.3 集成到CI/CD流水线将这套自动化模板集成到 CI/CD如 Jenkins, GitLab CI, GitHub Actions中才能最大化其价值。GitHub Actions 示例# .github/workflows/playwright-integrated.yml name: Playwright Integrated Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test-integrated: timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - name: Install Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install dependencies run: | pip install -r requirements.txt pip install pytest-playwright playwright install chromium - name: Run Integrated Tests env: BASE_URL: ${{ secrets.TEST_ENV_BASE_URL }} run: | pytest tests/integrated/ --projectapi-integration -v - name: Upload Playwright report if: always() uses: actions/upload-artifactv3 with: name: playwright-report-integrated path: playwright-report/ retention-days: 7 - name: Upload test results (JUnit) if: always() uses: actions/upload-artifactv3 with: name: junit-results-integrated path: test-results/ retention-days: 7CI集成要点环境变量通过secrets管理测试环境的地址、数据库连接等敏感信息。在配置中通过process.env.BASE_URL读取。指定项目运行使用--projectapi-integration来运行我们为集成测试单独配置的项目。产物归档将 HTML 报告和 JUnit 格式的结果文件作为产物保存便于失败时下载查看。稳定性集成测试可能因环境不稳定而偶发失败合理设置retries重试和timeout-minutes超时很重要。6. 常见问题与排查技巧实录在实际使用这套模板的过程中你肯定会遇到各种问题。下面是我踩过的一些坑和总结的排查思路。6.1 问题监听不到预期的API请求可能原因及解决方案问题现象可能原因排查步骤与解决方案完全无请求被捕获1. 监听器启动时机过晚。2. 请求在页面加载初期监听前就已发生。3. 请求来自 iframe 或 Service Worker。1.确保start_listening()在页面导航或触发操作前调用。最好在BasePage的__init__中调用。2. 使用page.on(request, ...)全局监听所有请求查看是否有目标请求发出确认其触发时机。3. 对于 iframe需要获取到 iframe 的page对象并在其上监听。page.frames可以获取所有 frame。请求被捕获但URL不匹配url_pattern匹配逻辑有误如大小写、查询参数。1. 打印出捕获到的所有请求 URL检查目标请求的实际格式。2. 使用更宽松的匹配模式如if ‘keyword’ in url或使用正则表达式进行匹配。Playwright 的page.route(‘**/api/*’, …)支持 glob 模式。请求被捕获但响应体为空或状态码错误1. 请求可能失败了4xx/5xx。2. 响应体可能不是 JSON或者很大读取时出错。1. 在_handle_response中检查response.ok或response.status。2. 增加异常处理对于非 JSON 响应尝试以文本形式读取。对于大响应体考虑只读取头部或特定字段。实操心得调试网络监听问题时最直接的方法是在_handle_response方法里加一行print(f”Response received: {response.url} - Status: {response.status}”)把所有捕获到的请求和状态都打印出来。这样你就能清晰地看到请求的流动快速定位是没抓到还是抓错了。6.2 问题测试用例在CI上不稳定Flaky Tests集成测试尤其是涉及 UI 和网络在 CI 环境中更容易出现不稳定的情况。应对策略增加智能等待减少硬性等待避免使用time.sleep()。多用 Playwright 内置的等待条件如page.wait_for_selector(selector, state‘visible’),page.wait_for_response(url),page.wait_for_function()等。它们会轮询直到条件满足比固定睡眠更高效、更稳定。启用重试机制在 Playwright 配置中设置retries。对于因网络瞬时波动或资源加载慢导致的失败重试往往能解决。但要注意对于逻辑错误重试只会浪费时间。使用trace进行事后分析如前所述配置trace: ‘on-first-retry’或‘retain-on-failure’。当测试在 CI 上失败时下载追踪文件在本地用playwright show-trace打开像看录像一样复盘测试执行过程能极大提升排查效率。隔离测试数据确保每个测试用例使用独立的数据集避免用例间因操作同一数据而产生冲突。这需要前后端配合例如使用独立的测试账号或通过 API 在setUp中创建数据在tearDown中清理。优化选择器避免依赖不稳定的属性优先使用># 脆弱的断言 assert api_response[body] expected_full_response_body # 健壮的断言 - 只关心核心业务字段 response_body api_response[body] assert response_body[success] is True assert response_body[orderId] is not None assert response_body[totalAmount] expected_amount # 忽略其他字段如 createdTime, message 等对于 UI 断言也一样优先断言关键文本和元素状态而不是整个 DOM 结构或精确的样式。构建这套 Playwright UI 与 API 接口自动化模板的过程实际上是在重新思考自动化测试的边界和价值。它要求测试者不仅关心“页面长什么样”更要理解“页面为什么长这样”。当你开始习惯从网络请求的视角去审视一次用户操作时你对系统行为的理解会深刻得多。这套模板最初可能会增加一些编写用例的复杂度但长远来看它带来的测试深度、稳定性和问题定位能力会让你的自动化测试工作事半功倍。最关键的是它推动了一种更贴近系统本质的测试思维方式。