基于Midscene.js构建跨平台AI自动化测试系统:从场景描述到智能执行
1. 项目概述为什么我们需要一个跨平台的AI自动化测试系统最近在跟几个测试团队的朋友聊天大家普遍都在头疼同一个问题现在的应用生态太“碎”了。一个产品往往要覆盖Web端、移动端iOS/Android、甚至桌面端。每次发版测试同学就得在Selenium、Appium、Playwright、Cypress等一堆工具之间反复横跳脚本维护成本高环境搭建复杂更别提不同平台上的断言逻辑和交互模式差异巨大一个简单的登录流程在不同端上可能要写三套完全不同的测试脚本。这还没算上UI元素频繁变动带来的脚本“脆弱性”问题。正是在这种背景下我注意到了Midscene.js。它不是一个简单的测试框架而是一个旨在用一套脚本、一种逻辑来驱动多端测试的“中枢神经系统”。更吸引人的是它宣称集成了AI能力能应对UI变化实现一定程度的“自适应”测试。这听起来像是解决我们痛点的理想方案。于是我花了近一个月时间深入研究了Midscene.js并尝试用它搭建一个从零开始的、轻量级的跨平台AI自动化测试系统。整个过程我把它提炼成了三个核心步骤希望能给同样被多端测试困扰的同行们一些实实在在的参考。2. 核心思路拆解Midscene.js如何实现“一套代码多端运行”在动手之前我们必须先理解Midscene.js的设计哲学。它和我们熟悉的Playwright、Selenium这类直接操作浏览器或设备原生API的框架有本质区别。2.1 架构核心场景描述与驱动解耦Midscene.js的核心思想是“场景描述与执行驱动分离”。这是什么意思呢传统的自动化测试脚本是这样的“用Chrome驱动找到ID为‘username’的输入框输入‘testexample.com’”。这个脚本和Chrome浏览器或WebDriver是强绑定的。换到移动端你就得换一套API和选择器。而Midscene.js要求我们换一种思维方式。我们不再直接命令浏览器而是描述一个用户场景。比如对于“登录”这个场景我们可以这样描述场景用户登录步骤在“用户名”输入区域填入凭证“user123”。在“密码”输入区域填入凭证“pass456”。点击标识为“登录”或“Sign In”的可操作区域。注意这里的描述是平台无关的。我们没有提到任何HTML的ID、CSS选择器或者移动端的resource-id、accessibility-id。我们只是在描述用户的意图和行为。那么谁来执行这些描述呢这就是Midscene.js的“驱动”层。Midscene.js自身或社区提供了针对不同平台的“驱动适配器”Driver Adapter例如midscene/driver-playwright: 将场景描述翻译成Playwright命令在Web端执行。midscene/driver-appium: 将同样的场景描述翻译成Appium命令在移动端执行。理论上还可以有驱动桌面应用、甚至IoT设备交互的适配器。你的测试脚本场景描述是稳定的而执行层是可以像插件一样替换的。这就实现了“一套代码多端运行”。2.2 AI能力的角色从“死定位”到“活理解”解决了多端驱动的问题UI变化导致的脚本脆弱性怎么办这就是Midscene.js集成AI通常是计算机视觉CV或大语言模型LLM的用武之地。传统脚本依赖精确的元素定位器XPath, CSS SelectorUI一改定位器就失效脚本就“崩”。Midscene.js引入的AI能力旨在让脚本具备一定的“理解”和“适应”能力。元素识别AI可以辅助识别元素。当你描述“点击标识为‘登录’的区域”时AI驱动不是去找一个固定的button#login而是去“看”当前屏幕利用OCR光学字符识别或图像特征匹配找到一个看起来像“登录”按钮的区域并点击。即使这个按钮的CSS类名从.btn-primary变成了.btn-submit只要视觉上它还是“登录”按钮AI就有可能找到它。场景理解与自愈更高级的应用是AI可以理解当前页面所处的状态。比如执行登录后如果AI“看到”了一个错误提示弹窗例如“密码错误”它可以基于预设的策略记录在场景描述中自动处理比如“如果发现包含‘错误’文本的弹窗则截图并标记测试失败”而不是让脚本因为找不到下一个预期元素而卡死。一个重要认知这里的AI不是取代测试工程师去写用例而是增强测试脚本的鲁棒性和表达力。你仍然需要定义清晰的测试场景和预期但AI帮助你以更接近人类用户的方式与软件交互减少了因UI细节变动而带来的维护成本。2.3 三步构建法的逻辑基于以上理解我规划的“三步构建法”逻辑如下第一步环境与核心搭建。解决“用什么”和“怎么连”的问题建立Midscene.js核心与AI服务的基础连接。第二步场景描述与驱动配置。解决“做什么”的问题用Midscene.js的语法编写平台无关的测试场景并配置好针对不同平台的执行驱动。第三步AI集成与流程串联。解决“如何更智能”的问题接入具体的AI服务如OpenAI的API或本地CV模型让测试脚本具备视觉理解和自适应能力并将整个流程串起来形成可执行的测试系统。3. 第一步环境准备与Midscene.js核心框架搭建万事开头难第一步的目标是把地基打牢确保Midscene.js的核心运行环境以及它与AI“大脑”的通信是畅通的。3.1 技术栈选型与初始化我选择Node.js作为运行环境因为它生态丰富且Midscene.js本身就是基于Node.js的。同时为了后续集成AI的便利性我决定使用TypeScript来获得更好的类型提示和代码可维护性。# 1. 初始化项目 mkdir midscene-ai-test-system cd midscene-ai-test-system npm init -y # 2. 安装TypeScript及相关类型定义 npm install typescript ts-node types/node --save-dev # 3. 初始化tsconfig.json npx tsc --init # 在生成的tsconfig.json中确保以下配置合适 # target: ES2020, # module: commonjs, # outDir: ./dist, # rootDir: ./src, # strict: true # 4. 安装Midscene.js核心库 npm install midscene注意Midscene.js作为一个较新的框架其API和生态可能变化较快。建议在安装时指定版本或查阅其官方文档获取最新的安装指南。有时核心库和驱动包是分开的。3.2 配置AI服务连接以OpenAI为例Midscene.js的AI能力通常需要通过外部服务来提供。这里我以OpenAI的API为例因为它易于接入且其GPT-4V视觉模型非常适合处理“看图说话”类的UI理解任务。首先安装OpenAI的Node.js SDKnpm install openai然后创建一个环境配置文件.env来管理敏感信息并使用dotenv包来读取npm install dotenv.env文件内容OPENAI_API_KEYsk-your-actual-api-key-here # 可以定义其他配置如模型选择、超时时间等 OPENAI_MODELgpt-4-vision-preview OPENAI_TIMEOUT30000接下来创建一个AI服务模块src/ai-service.ts封装与OpenAI的交互import { Configuration, OpenAIApi } from openai; import * as dotenv from dotenv; dotenv.config(); export class AIService { private openai: OpenAIApi; constructor() { const configuration new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); this.openai new OpenAIApi(configuration); } /** * 分析屏幕截图描述其内容或回答问题 * param imageBase64 屏幕截图的Base64字符串 * param prompt 给AI的指令例如“画面上有哪些可点击的按钮” */ async analyzeScreen(imageBase64: string, prompt: string): Promisestring { try { const response await this.openai.createChatCompletion({ model: process.env.OPENAI_MODEL || gpt-4-vision-preview, messages: [ { role: user, content: [ { type: text, text: prompt }, { type: image_url, image_url: { url: data:image/png;base64,${imageBase64}, }, }, ], }, ], max_tokens: 500, }); return response.data.choices[0]?.message?.content?.trim() || AI分析无返回; } catch (error) { console.error(调用AI服务失败:, error); throw new Error(AI分析失败: ${error.message}); } } /** * 基于当前页面状态决定下一步操作简化示例 * param pageDescription 当前页面的文字描述 */ async decideNextAction(pageDescription: string): Promisestring { const prompt 你是一个自动化测试助手。当前页面状态描述如下${pageDescription}。我们的目标是登录系统。请告诉我下一步最可能执行的操作是什么只需回复一个简短动作例如“输入用户名”、“输入密码”、“点击登录按钮”、“等待加载”。; const response await this.openai.createChatCompletion({ model: gpt-4, messages: [{ role: user, content: prompt }], max_tokens: 50, }); return response.data.choices[0]?.message?.content?.trim(); } }这个模块提供了两个核心方法analyzeScreen用于视觉分析decideNextAction用于基于文本描述的决策。它们是后续让测试脚本“变聪明”的基础。3.3 创建Midscene.js运行上下文Midscene.js的执行需要一个“运行时”来管理场景和驱动。我们创建一个简单的启动文件src/runner.tsimport { MidsceneEngine } from midscene; // 注意这里先不引入具体驱动驱动在第二步配置 import { AIService } from ./ai-service; export class TestRunner { private engine: MidsceneEngine; private aiService: AIService; constructor() { this.engine new MidsceneEngine(); this.aiService new AIService(); // 这里可以将aiService注册到engine的某个扩展点具体方式取决于Midscene.js的插件机制 // 假设我们通过一个自定义的“AI上下文”来传递 (this.engine as any).aiContext { service: this.aiService }; } getEngine(): MidsceneEngine { return this.engine; } getAIService(): AIService { return this.aiService; } async runScene(sceneDefinition: any): Promisevoid { // 加载场景定义 this.engine.loadScene(sceneDefinition); // 执行场景 await this.engine.execute(); } }至此我们的系统骨架就搭起来了。它有了项目基础、AI服务连接、和Midscene.js的运行引擎。但此时它还什么都做不了因为我们还没有定义任何测试场景也没有告诉它如何去操作具体的设备或浏览器。这就是第二步要解决的问题。4. 第二步编写跨平台场景与配置多端驱动这一步是核心中的核心我们要用Midscene.js的方式描述测试并让它能在不同平台上“动起来”。4.1 定义平台无关的测试场景我们以一个经典的“用户登录”场景为例。在src/scenes/login-scene.ts中我们不用任何平台特定的代码。// 这是一个符合Midscene.js预期的场景定义对象 export const loginScene { id: user-login-flow, name: 用户登录流程, meta: { description: 验证用户可以使用有效凭证登录系统, targetPlatforms: [web, android, ios], // 声明此场景目标平台 }, steps: [ { id: navigate-to-login, action: navigate, parameters: { url: {{baseUrl}}/login, // 使用变量不同平台可配置不同的baseUrl }, assertions: [ { type: url_contains, value: login, }, ], }, { id: fill-username, action: fill, // 关键点这里不使用CSS选择器而是使用“角色”或“标识” target: { // 方案A使用语义化标识依赖AI或应用的可访问性属性 identifier: { role: textbox, name: 用户名 }, // 方案B使用相对位置或图像特征更依赖AI视觉 // identifier: { image: username_field_template.png } }, parameters: { value: {{username}}, }, }, { id: fill-password, action: fill, target: { identifier: { role: textbox, name: 密码, secure: true }, }, parameters: { value: {{password}}, }, }, { id: click-login-button, action: click, target: { // 标识为一个名为“登录”的按钮 identifier: { role: button, name: 登录 }, }, }, { id: verify-login-success, action: wait_for, target: { identifier: { role: heading, name: 欢迎面板 }, }, parameters: { timeout: 10000, }, assertions: [ { type: element_visible, target: { identifier: { role: heading, name: 欢迎面板 } }, }, // 可以添加更复杂的AI断言例如“请确认页面中央显示‘登录成功’的提示” { type: ai_assert, parameters: { prompt: 请分析当前屏幕确认是否包含“登录成功”或“Welcome”的文本提示并判断用户头像是否显示。, expected: 应包含成功登录的文本提示和用户头像。, }, }, ], }, ], // 场景级别的变量 variables: { baseUrl: https://myapp.example.com, // Web端 username: test_user, password: secure_pass_123, }, // 钩子函数可用于注入AI逻辑 hooks: { beforeStep: async (step, context) { // 示例在每一步执行前可以用AI分析当前屏幕辅助定位元素或决策 const aiService context.aiContext?.service; if (aiService step.target?.identifier?.name) { const screenshot await context.driver.takeScreenshot(); // 假设driver有该方法 const analysis await aiService.analyzeScreen( screenshot, 请帮我找到名为“${step.target.identifier.name}”的元素并描述其位置或特征。 ); console.log(AI辅助定位建议: ${analysis}); // 可以将分析结果用于调整step.target这是一个高级用法 } }, }, };这个场景定义完全脱离了具体平台。target不是#login-btn而是{ role: button, name: 登录 }。这依赖于应用本身提供了良好的可访问性属性如ARIA标签或者依赖我们在下一步配置的驱动和AI能力去“理解”这个描述并找到对应元素。4.2 配置与注册平台驱动现在我们需要为这个场景配上“手”和“脚”。假设我们要支持Web用Playwright和Android用Appium。首先安装社区驱动或官方驱动包这里以假设的包名为例npm install midscene/driver-playwright midscene/driver-appium然后创建一个驱动管理器src/driver-manager.tsimport { PlaywrightDriver } from midscene/driver-playwright; import { AppiumDriver } from midscene/driver-appium; import { TestRunner } from ./runner; export async function setupWebDriver(runner: TestRunner, browserType: chromium | firefox | webkit chromium) { const playwrightDriver new PlaywrightDriver({ browserType, headless: false, // 调试时可设为true viewport: { width: 1280, height: 720 }, }); await playwrightDriver.init(); // 将驱动注册到Midscene.js引擎 runner.getEngine().registerDriver(web, playwrightDriver); // 为Web场景覆盖变量例如不同的baseUrl runner.getEngine().setVariable(baseUrl, https://myapp-web.example.com); } export async function setupAndroidDriver(runner: TestRunner, deviceName: string, appPath: string) { const appiumDriver new AppiumDriver({ platformName: Android, appium:deviceName: deviceName, appium:app: appPath, appium:automationName: UiAutomator2, appium:noReset: false, }); await appiumDriver.init(); runner.getEngine().registerDriver(android, appiumDriver); runner.getEngine().setVariable(baseUrl, myapp://); // 移动端可能是Deep Link或应用内页面 }关键点同一个loginScene在注册了web驱动后Midscene.js会使用PlaywrightDriver来解析场景步骤。当执行到click一个{role: button, name: 登录}的元素时PlaywrightDriver会尝试在页面上找到一个可访问性名称aria-label或文本内容为“登录”的按钮来点击。对于Android驱动AppiumDriver则会尝试通过content-desc、text或resource-id等属性来定位同名元素。4.3 编写场景执行脚本最后我们创建一个主执行文件src/index.ts将场景、驱动和运行器组合起来import { TestRunner } from ./runner; import { setupWebDriver } from ./driver-manager; import { loginScene } from ./scenes/login-scene; async function main() { const runner new TestRunner(); // 根据命令行参数或配置决定运行哪个平台 const targetPlatform process.env.TARGET_PLATFORM || web; switch (targetPlatform) { case web: await setupWebDriver(runner); // 可以动态修改场景变量例如针对Web的测试账号 loginScene.variables.username web_test_user; break; case android: // 需要传入真实的设备名和APP路径 // await setupAndroidDriver(runner, emulator-5554, ./app/android/app-debug.apk); // loginScene.variables.username android_test_user; console.log(Android驱动配置示例请填写真实参数。); return; default: console.error(不支持的平台: ${targetPlatform}); return; } console.log(开始在${targetPlatform}平台执行场景: ${loginScene.name}); try { await runner.runScene(loginScene); console.log(场景执行成功); } catch (error) { console.error(场景执行失败:, error); // 这里可以加入失败截图、日志上报等逻辑 process.exit(1); } finally { // 清理资源如关闭浏览器、断开Appium连接 await runner.getEngine().cleanup(); } } main();现在执行npx ts-node src/index.ts并设置TARGET_PLATFORMweb你应该能看到一个浏览器启动并自动执行登录流程。同样的场景定义只需更换驱动注册就可以在Android模拟器或真机上运行。跨平台的核心目标已经达成。5. 第三步深度集成AI与构建自动化流程第二步我们实现了跨平台但元素的定位很大程度上还依赖于应用本身提供的可访问性属性。如果应用没有这些属性或者UI是自定义绘制的如游戏、Canvas脚本就会失败。第三步的目标是利用AI来弥补这一缺陷并构建一个更智能、更健壮的自动化流程。5.1 实现AI辅助元素定位当标准的role和name定位失败时我们可以降级使用AI视觉定位。我们需要增强我们的驱动或场景钩子。修改src/ai-service.ts增加一个专门的视觉定位方法export class AIService { // ... 其他代码 ... /** * 通过视觉识别定位元素 * param screenshotBase64 当前屏幕截图 * param elementDescription 要查找元素的描述如“蓝色的登录按钮” * returns 返回元素在屏幕上的大致坐标区域 { x, y, width, height } */ async locateElementByVision( screenshotBase64: string, elementDescription: string ): Promise{ x: number; y: number; width: number; height: number } | null { const prompt 你是一个UI分析助手。请分析下面的屏幕截图。 我需要你找到这个元素“${elementDescription}”。 请仅返回一个JSON对象格式为 { x: number, y: number, width: number, height: number }代表该元素在图片中的大致边界框坐标。 图片是${screenshotWidth}x${screenshotHeight}像素。如果找不到返回null。 ; const response await this.openai.createChatCompletion({ model: process.env.OPENAI_MODEL || gpt-4-vision-preview, messages: [{ role: user, content: [{ type: text, text: prompt }, { type: image_url, image_url: { url: data:image/png;base64,${screenshotBase64} } }] }], max_tokens: 200, }); const content response.data.choices[0]?.message?.content; try { const result JSON.parse(content.trim()); return result; } catch (e) { console.warn(AI返回的坐标解析失败:, content); return null; } } }然后我们可以创建一个“混合驱动”或“智能钩子”在标准定位失败时调用AI视觉定位进行兜底。例如在场景的beforeStep钩子中实现// 在login-scene.ts的hooks.beforeStep中增强 hooks: { beforeStep: async (step, context) { const driver context.driver; const aiService context.aiContext?.service; // 如果步骤有目标元素且驱动支持截图 if (step.target driver.takeScreenshot aiService) { try { // 1. 首先尝试标准方式查找元素由驱动内部实现 const element await driver.findElement(step.target.identifier); if (element) { context.currentElement element; // 将找到的元素存入上下文 return; // 找到则直接返回 } } catch (standardLocatorError) { // 标准定位失败记录日志并尝试AI视觉定位 console.log(标准定位失败尝试AI视觉定位: ${step.target.identifier.name}); } // 2. AI视觉定位兜底 const screenshot await driver.takeScreenshot(); const coordinates await aiService.locateElementByVision( screenshot, 一个名为“${step.target.identifier.name}”的${step.target.identifier.role}元素 ); if (coordinates) { console.log(AI视觉定位成功坐标: ${JSON.stringify(coordinates)}); // 将坐标转换为驱动可执行的操作如点击、输入 // 这需要驱动支持通过坐标操作或者我们模拟一个点击事件 await driver.clickAtCoordinates(coordinates.x coordinates.width/2, coordinates.y coordinates.height/2); // 标记此步骤已通过AI方式处理跳过驱动的默认执行 step._handledByAI true; } else { throw new Error(AI也无法定位元素: ${step.target.identifier.name}); } } }, }实操心得AI视觉定位的准确性和速度是挑战。坐标可能不准且API调用有延迟和成本。因此它绝不能作为首选方案而应作为当应用缺乏可访问性支持时的最终兜底手段。在实际项目中应优先推动开发团队为UI元素添加完善的测试属性如>// 在runner.ts或一个专门的断言处理器中 async function handleAIAssertion(assertion, context) { const { prompt, expected } assertion.parameters; const aiService context.aiContext.service; const driver context.driver; const screenshot await driver.takeScreenshot(); const aiAnalysis await aiService.analyzeScreen(screenshot, prompt); console.log(AI断言分析结果: ${aiAnalysis}); // 简单的关键词匹配更复杂的可以用AI再次判断分析结果是否符合预期 if (expected !aiAnalysis.includes(expected)) { // 这是一个非常简单的示例 throw new Error(AI断言失败。预期包含“${expected}”但AI分析结果为“${aiAnalysis}”); } // 也可以让AI自己判断是否通过 const verificationPrompt 根据之前的分析“${aiAnalysis}”我们的预期是“${expected}”。这个预期是否得到了满足只需回答“是”或“否”。; const verification await aiService.decideNextAction(verificationPrompt); if (verification.trim() ! 是) { throw new Error(AI验证断言失败。); } }这种断言非常适合验证非结构化的页面内容例如“确认订单成功页面包含订单号和‘支付成功’字样。”“检查图表是否正常渲染没有出现错误提示。”“验证 toast 提示消息的文本和颜色是否正确。”5.3 构建完整的自动化测试流程将以上所有部分串联起来我们就得到了一个完整的、支持AI兜底的跨平台自动化测试系统。其工作流程如下启动根据配置如TARGET_PLATFORM初始化对应的驱动Playwright/Appium和AI服务。加载场景读取平台无关的场景描述文件如loginScene。执行步骤对场景中的每一步标准执行驱动尝试使用场景中定义的语义化标识role,name定位元素并执行操作。AI兜底如果标准定位失败触发AI视觉定位流程通过截图和AI分析找到元素坐标并执行操作。AI断言如果步骤中包含ai_assert则在步骤执行后调用AI分析屏幕进行结果验证。生成报告收集每一步的执行结果、日志、截图尤其是AI介入和断言时的截图生成一份人类可读的测试报告。清理关闭浏览器、释放移动设备、结束会话。我们可以使用Jest、Mocha等测试运行器来组织多个场景并生成更漂亮的报告。也可以将其集成到CI/CD流水线中在每次构建后自动对Web、Android、iOS等多端进行冒烟测试。6. 常见问题、踩坑记录与优化建议在实际搭建和实验过程中我遇到了不少问题也总结出一些让系统更稳定的经验。6.1 稳定性与性能问题AI响应延迟与成本每次调用GPT-4V API都需要时间几秒到十几秒和费用。解决方案缓存对相同的屏幕和提示缓存AI分析结果。降级策略仅在标准定位失败时使用AI并设置超时和重试次数限制。使用轻量级模型对于简单的元素识别可以考虑使用本地运行的、专用的计算机视觉模型如YOLO做目标检测速度更快且零成本。视觉定位不准AI返回的坐标框可能偏移或大小不对。解决方案坐标后处理在点击前可以在坐标点附近小范围内随机偏移模拟真人操作。结合多种定位方式AI定位后可以尝试用找到的区域内的文本或属性进行二次精确定位。训练专属模型如果应用UI风格固定可以收集截图标注数据训练一个专门识别该应用UI元素的模型精度会远高于通用模型。6.2 场景设计的可维护性变量与数据驱动像{{baseUrl}}、{{username}}这样的变量非常有用。应该将所有环境相关的配置URL、账号、设备信息和测试数据商品ID、订单号外置到配置文件如config/prod.json,config/staging.json或数据文件中场景文件只保留业务流程。页面对象模型Page Object的融合虽然Midscene.js提倡场景描述但对于中大型项目完全抛弃页面对象可能造成元素标识符重复定义。可以折中定义“页面片段”Page Fragment将一组相关的元素标识符如登录页面的用户名框、密码框、按钮封装成一个可复用的对象在多个场景中引用。断言过于依赖AIAI断言不稳定且慢。最佳实践是混合断言核心业务流程的关键节点如跳转URL、重要元素出现使用快速、稳定的传统断言对于视觉验证、复杂文本内容校验等使用AI断言作为补充。6.3 调试与日志详尽的截图和日志务必在每一步尤其是AI介入和断言时保存屏幕截图和上下文日志。这不仅是排查问题的依据也是理解AI决策过程的关键。交互式调试模式可以开发一个模式让测试在每一步暂停并展示AI对当前屏幕的分析结果和下一步建议方便测试人员复核和调试AI的行为。6.4 对团队的要求引入这样的系统不仅仅是技术变革也对团队协作提出了新要求开发侧需要更有意识地为UI元素添加可访问性属性如aria-label,testID这是提高自动化稳定性和效率的最廉价、最有效的方法。测试侧需要学习新的场景描述语言理解AI的能力和局限从编写“操作脚本”转向设计“用户意图流”。流程侧需要将AI服务的成本、密钥管理、模型版本更新等纳入DevOps流程进行管理。搭建基于Midscene.js的跨平台AI自动化测试系统是一个从“操作模拟”向“意图驱动”和“智能适应”演进的过程。它不能解决所有问题初期可能会因为AI的不稳定性引入新的复杂度。但对于UI频繁迭代、需要覆盖多端且追求测试脚本长期可维护性的团队来说这是一条值得探索的前沿路径。我的实践表明通过“标准定位为主AI视觉为辅”的混合策略可以在保持核心脚本稳定的前提下显著提升对UI变化的容忍度。最关键的是它迫使我们去思考测试的本质——我们不是在测试代码而是在验证用户场景是否能被正确执行。