📝 面试求职: 「面试试题小程序」 ,内容涵盖 测试基础、Linux操作系统、MySQL数据库、Web功能测试、接口测试、APPium移动端测试、Python知识、Selenium自动化测试相关、性能测试、性能测试、计算机网络知识、Jmeter、HR面试,命中率杠杠的。(大家刷起来…)

📝 职场经验干货:

软件测试工程师简历上如何编写个人信息(一周8个面试)

软件测试工程师简历上如何编写专业技能(一周8个面试)

软件测试工程师简历上如何编写项目经验(一周8个面试)

软件测试工程师简历上如何编写个人荣誉(一周8个面试)

软件测试行情分享(这些都不了解就别贸然冲了.)

软件测试面试重点,搞清楚这些轻松拿到年薪30W+

软件测试面试刷题小程序免费使用(永久使用)


年后重新开始看效能相关的任务,由于去年主要在接口自动化,所以最近开始做一些 UI 自动化的东西。 目前实践了一个基于视觉驱动的 Midscene,另一个基于单模态大模型驱动的 Stagehand。 今天这篇主要介绍一下 Midscene。

PS: 我之前有过一个使用 cursor 来生成测试用例和自动化测试代码的直播录屏,还没看的同学可以看一下:https://testerhome.com/articles/43375

一、Midscene 是什么?

Midscene.js 是字节跳动 Web Infra 团队开源的 AI 驱动 UI 自动化框架。它的核心理念是:用自然语言描述操作意图,由 AI 来理解页面并执行操作,而不是让你写一堆 CSS 选择器。 比如:

图片

如上图所示,控件的定位方式从以前的 xpath 和 css selector,变成了一段自然语言。控件定位的形式发生了变化,变得更简单,更灵活。只要 UI 界面没有发生重大变化,多模态大模型都可以自动调整,找到正确的控件。因为模型会通过截图和用户提供的定位控件的自然语言的描述,找到对应控件的正确位置并进行操作。 即便前端开发修改了 DOM 树结构也没有关系。

AI 驱动的两种模式

以我的理解, AI 驱动 UI 自动化其实有两种实践方式, 一种是我们上面看到的在运行时使用模型来进行控件定位。而另一种其实是利用 AI 编程工具,辅助编写 UI 自动化测试代码。 在这个阶段, cursor/codebuddy/codex 这些 AI 辅助工具会动态启动浏览器,动态获取 DOM 树,自动识别控件的定位方式(xpath/css selector, 也可以是自然语言驱动的形式),如下图:

图片

Midscene 核心能力

能力

说明

aiInput(locatePrompt, { value })

找到输入框并输入内容

aiTap(locatePrompt)

找到元素并点击

aiAssert(prompt)

视觉断言(判断页面状态)

aiQuery(prompt)

从页面提取信息

constagent=newPlaywrightAgent(page);

// 不需要知道是什么 CSS,直接描述
awaitagent.aiInput('找到搜索输入框',{value:'提示词模板'});
awaitagent.aiTap('点击放大镜搜索按钮');
awaitagent.aiAssert('搜索结果列表已显示');

运行原理

  1. 截图捕获

     → page.screenshot() 获取当前视口图像

  2. DOM 提取

     → 同时抓取可访问性树,提供双重上下文

  3. 模型推理

     → 截图 + 操作意图发送给视觉大模型(Qwen-VL 等)

  4. 坐标反推

     → 模型识别目标元素位置,转换为可执行 locator

  5. 执行操作

     → Playwright 真实点击/输入,生成带截图的可视化报告


二、用CodeBuddy/cursor/codex/CC + MCP + Skills编写测试用例

传统方式:手动打开浏览器 → 看 DOM 结构 → 手写选择器 → 写用例代码,流程割裂、效率低。

新方式:AI IDE 全程辅助,从需求到代码一气呵成。

IDE辅助编写UI自动化的整体架构

图片

再看这张图展示了完整的工具链分层架构:

层次

组成

职责

AI Agent 层

Claude Code / Cursor / CodeBuddy

理解测试需求,生成符合规范的测试代码

MCP 协议层

Playwright MCP Server(探索/调试)
Playwright CLI + Skills(批量/CI)

连接 AI 与浏览器,提供工具调用能力

浏览器层

Chromium / Firefox / WebKit

真实浏览器执行,支持截图与 DOM 分析

被测平台

登录页 · 应用列表 · 对话页 · 工作流编辑页

实际被测系统,AI 直接感知页面状态

两条路径的分工:

  • 探索性测试/调试

    :通过 Playwright MCP Server,AI 实时操控浏览器,适合开发期探索页面结构、生成初始测试代码

  • 批量测试/CI

    :通过 Playwright CLI + Skills,在 CI 流水线中批量执行,Skills 保证代码始终符合项目规范

工具链架构

AI IDE
    ↓ 理解需求意图
Skills 
    ↓ 注入项目架构规范、定位优先级规则
MCP Browser Tool
    ↓ 实时打开目标页面,抓取真实 DOM
生成符合规范的 TypeScript 测试代码

Skills的关键作用

在项目根目录的 rules.md 中定义项目规范,AI IDE 会自动将其注入到 AI 上下文中。这意味着 AI 每次生成代码都「知道」:

  • 项目是四层架构,禁止用例层 new XxxPage()

  • 定位优先级:id > name > type > 语义class > 文案

  • v-select 组件有双 DOM 陷阱,必须用 :not(.is-hidden) 过滤

  • 应用名称必须用 randomAppName() 生成,末尾带 _qta

效果:AI 不会再犯已知错误,生成代码直接符合团队规范,无需反复纠正。

MCP浏览器工具

配合 Playwright MCP 工具,AI IDE 可以:

  • 实时导航到目标页面

  • 抓取真实 DOM 结构(而非猜测)

  • 根据真实 DOM 选择最优定位方式


三、四层分层架构设计

整体来说 rules.md 是告诉 AI 应该如何生成代码的,本质上,我们的 UI 自动化框架,应该仍然是我之前分享过的 4 层架构:

图片

对这个架构不了解的同学可以访问:https://testerhome.com/articles/31647

tests/
├── components/base/     # 【组件层】可复用 UI 控件原子操作
│   ├── select.component.ts          # v-select 封装(双DOM陷阱)
│   ├── search-input.component.ts    # 搜索框(AI 驱动)
│   └── captcha.component.ts         # 验证码
├── pages/           # 【页面层】单页面操作封装
│   ├── login.page.ts
│   ├── home.page.ts
│   └── agent-editor.page.ts
├── services/        # 【业务逻辑层】完整业务流程
│   ├── auth.service.ts              # 登录流程
│   └── app.service.ts               # 创建应用流程
└── cases/           # 【用例层】测试用例 + 断言
    ├── login.spec.ts
    └── optimize-prompt.spec.ts

各层职责边界

职责

禁止

组件层

UI 控件原子操作封装

不得依赖 pages/services

页面层

单页面操作组合

不得跨页面,不得写 expect

业务逻辑层

跨页面完整流程

不得直接操作 DOM

用例层

业务编排 + 断言

不得直接 new XxxPage()

这里可以给出,我给大模型的提示词:

我现在要涉及一个专业的,企业级的 UI自动化测试项目。 所以我需要把代码进行分层。

1. 组件层:因为前端也都是组件化开发的,所以各个组件都是复用的。 比如某个button的放在了多个界面上,所以它的定位方式是一样的。 所以我需要单独一层来进行封装这些组件的定位方式。
2. Page层(页面层):封装单独页面的操作:
3. Service层:在页面层之上,封装成熟的业务逻辑, 可能会调用多个Page的的逻辑。
4. case层:编写测试用例的地方。 

上面是我当初初始化项目的时候,用的提示词。


四、混合定位策略:稳定用传统,易变用AI

这是最重要的经验总结。 Midscene 的基于计算机视觉的控件定位方式虽然好,但它有几个缺点:

  • 很烧 token,如果每个控件都走 AI 识别,那必定是一笔非常大的支出。

  • 视觉方案执行很慢:我用的公司内部署的多模态大模型,识别一个控件的速度大约在 5s 左右。毕竟要把截图发送给大模型进行识别,这个识别速度很难保证。并且你也无法保证大模型的效果如何,万一大模型抽风,识别错了,就尴尬了。

所以我们注定要使用传统定位方式 +AI 驱动的方式来进行定位。 下面我介绍几个核心原则。

核心原则

不是所有定位都需要 AI,AI 定位有真实的 Token 成本。

传统定位:适用场景

当元素有稳定的 HTML 属性时,永远优先用传统定位

// ✅ 表单控件有 name 属性 → 最优选
page.locator('input[name="loginName"]')
page.locator('input[name="password"]')

// ✅ 业务语义 class → 稳定
page.locator('button.add-agent')
page.locator('button.add-plugin-btn')

// ✅ v-select 双DOM陷阱专用写法
page.locator('.v-popper.v-select__popper:not(.is-hidden) li:has-text("Multi-Agent模式")')

AI定位:适用场景

当元素属性不稳定、无语义属性、或是复杂控件时,降级用 AI:

// ✅ 搜索框 → 无稳定属性
awaitagent.aiInput('找到搜索输入框',{value:keyword});

// ✅ CodeMirror 富文本编辑器 → 无 value 属性
awaitagent.aiInput('找到提示词编辑区域,在"提示词"文案下方',{value:content});

// ✅ 图标型按钮 → 无文案
awaitagent.aiTap('点击放大镜搜索按钮');

// ✅ Tab 切换 → class 不稳定
awaitagent.aiTap('点击"自定义模板" Tab');

五、使用的几个技巧

注意:经过我的计算,每次 AI 调用(aiTap/aiInput)都需要截图并发送给视觉模型,约消耗 500-1500 token。

技巧1:优先传统定位

最直接的节省方式。90% 的元素都有稳定属性,只有真正不稳定的才用 AI。

技巧2:AI 调用前等待页面稳定

asyncfillPromptContent(content:string){
awaitthis.page.waitForTimeout(5_000);// 等待编辑器渲染完毕
constagent=newPlaywrightAgent(this.page);
awaitagent.aiInput('找到提示词编辑区',{value:content});
}

页面未稳定时 AI 可能定位失败触发重试,反而消耗更多 token。

技巧3:封装复用,避免重复写AI指令,并且midscence有缓存,可能利用缓存(它使用自然语言的描述作为缓存的key)

// ✅ 封装为组件层,多处复用同一 AI 指令
exportclassSearchInputComponent{
asyncsearch(keyword:string){
constagent=newPlaywrightAgent(this.page);
awaitagent.aiInput('找到搜索输入框',{value:keyword});
awaitagent.aiTap('点击放大镜搜索按钮');
}
}

技巧4:精确描述缩小搜索范围

// ❌ 模糊描述 → 模型需要分析整个页面
awaitagent.aiTap('找到输入框');

// ✅ 精确描述 → 模型快速定位
awaitagent.aiInput('找到"提示词"文案下方的富文本编辑区域',{value:content});

技巧5:合并连续操作用 aiAction

// ❌ 3 次截图
awaitagent.aiTap('点击新建按钮');
awaitagent.aiInput('找到标题输入框',{value:title});
awaitagent.aiTap('点击提交按钮');

// ✅ 1 次截图(连续操作合并)
awaitagent.aiAction(`点击新建按钮,填写标题"${title}",点击提交`);

六、完整实战教程:用CodeBuddy + Midscene从零搭建UI自动化测试

以一个通用 Todo 应用(增删改查)为例,完整演示:环境搭建 → 配置多模态模型 → 用 CodeBuddy 对话生成用例 → 使用 AI 视觉 API 操作页面 → 运行验证。


Step1:安装依赖

mkdir my-ui-test &&cd my-ui-test
npm init -y

# Playwright 测试框架
npm install-D @playwright/test
npx playwright install chromium

# Midscene Playwright 集成包
npm install-D @midscene/web

# 加载 .env 的工具
npm install-D dotenv

Step2:配置多模态视觉模型

Midscene 需要一个支持视觉的多模态大模型(VL Model)。模型通过 .env 文件配置,支持所有兼容 OpenAI 接口的视觉模型。

创建 .env 文件:

# ── 方案一:使用通义千问 Qwen-VL(推荐,官方支持)──────────────────
MIDSCENE_MODEL_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
MIDSCENE_MODEL_API_KEY="sk-你的阿里云DashScope密钥"
MIDSCENE_MODEL_NAME="qwen-vl-max-latest"
MIDSCENE_MODEL_FAMILY="qwen2.5-vl"

# ── 方案二:使用 Google Gemini ─────────────────────────────────────
# MIDSCENE_MODEL_BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai"
# MIDSCENE_MODEL_API_KEY="你的Gemini API Key"
# MIDSCENE_MODEL_NAME="gemini-2.0-flash"
# MIDSCENE_MODEL_FAMILY="gemini"

# ── 方案三:使用智谱 GLM-4V ───────────────────────────────────────
# MIDSCENE_MODEL_BASE_URL="https://open.bigmodel.cn/api/paas/v4"
# MIDSCENE_MODEL_API_KEY="你的智谱API Key"
# MIDSCENE_MODEL_NAME="glm-4v-plus"
# MIDSCENE_MODEL_FAMILY="glm-v"

# ── 方案四:使用企业自建兼容接口(如腾讯混元)──────────────────────
# MIDSCENE_MODEL_BASE_URL="https://你的企业模型地址"
# MIDSCENE_MODEL_API_KEY="sk-你的密钥"
# MIDSCENE_MODEL_NAME="模型名称/视觉模型"
# 注意:若模型不在官方支持列表,注释掉 MIDSCENE_MODEL_FAMILY 即可

MIDSCENE_MODEL_FAMILY 支持的官方值:

适用模型

qwen2.5-vl

阿里通义千问 Qwen-VL 系列

gemini

Google Gemini 系列

glm-v

智谱 GLM-4V 系列

doubao-vision

字节豆包视觉系列

gpt-5

OpenAI GPT-5

注意MIDSCENE_MODEL_FAMILY 必须填官方支持的值,否则报 Invalid MIDSCENE_MODEL_FAMILY value。如果你的模型不在列表中(如企业私有化部署),注释掉这行,Midscene 会跳过校验直接调用。


Step3:配置 Playwright 和环境加载

playwright.config.ts

import{defineConfig}from'@playwright/test';
import*asdotenvfrom'dotenv';

// 主进程加载 .env(worker 进程需要额外处理,见下方)
dotenv.config();

exportdefaultdefineConfig({
testDir:'./tests',
timeout:120_000,// AI 推理需要时间,超时设长一些
use:{
headless:false,// 建议非 headless,便于观察 AI 操作过程
baseURL:'http://localhost:3000',
},
});

tests/helpers/setup.ts(解决 worker 进程读不到 .env 的问题)

Playwright 每个测试文件在独立 worker 进程中运行,主进程的 dotenv.config() 不会传递给 worker。需要在每个 spec 文件中手动加载一次。

import*asdotenvfrom'dotenv';
import*aspathfrom'path';

dotenv.config({
path:path.resolve(__dirname,'../../.env'),
override:true,// 覆盖已有变量,确保 worker 中生效
});

Step4:用 CodeBuddy 对话生成测试用例

这是整个体系最核心的效率来源。不需要手写所有代码——直接和 CodeBuddy 对话,描述测试场景,AI 帮你生成符合规范的代码。

典型对话流程:


你:

我要测试 Todo 应用的新建任务功能。访问 http://localhost:3000,
页面上有一个输入框和"添加"按钮。
请帮我写一个 Playwright + Midscene 的测试用例:
1. 打开页面
2. 在输入框里输入"买牛奶"
3. 点击"添加"按钮
4. 断言列表中出现了"买牛奶"这条记录

输入框没有 id,但有 placeholder="请输入任务名称"。
"添加"按钮是 <button type="submit">。

CodeBuddy:

import{test,expect}from'@playwright/test';
import{PlaywrightAgent}from'@midscene/web/playwright';

test('新建 Todo 任务',async({page})=>{
// 打开页面
awaitpage.goto('http://localhost:3000');

// 输入框有 placeholder 属性,用传统定位(稳定)
awaitpage.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');

// 提交按钮有 type 属性,用传统定位
awaitpage.locator('button[type="submit"]').click();

// 断言列表中出现了新任务
awaitexpect(page.locator('li:has-text("买牛奶")')).toBeVisible();
});

你(追问):

任务列表是动态渲染的,有时候加载慢。另外"删除"按钮是一个垃圾桶图标,
没有文案也没有 id,我想补充一个删除的用例,用 AI 视觉来找到它。

CodeBuddy:

test('删除 Todo 任务',async({page})=>{
awaitpage.goto('http://localhost:3000');

// 先创建一条任务
awaitpage.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');
awaitpage.locator('button[type="submit"]').click();

// 等待列表渲染完成
awaitpage.locator('li:has-text("买牛奶")').waitFor({state:'visible'});

// 删除按钮是图标,没有文案,用 AI 视觉定位
constagent=newPlaywrightAgent(page);
awaitagent.aiTap('"买牛奶"这条任务右侧的删除按钮(垃圾桶图标)');

// 断言任务已消失
awaitexpect(page.locator('li:has-text("买牛奶")')).not.toBeVisible();
});

你(继续):

帮我把这两个用例整理成规范的 Page Object 结构,把页面操作封装到 page 层,
用例层只调用方法和写断言。

CodeBuddy 会按你的架构规范自动生成完整的分层代码(如果你配置了 .codebuddy/rules.md,生成的代码会直接符合团队规范)。

在 AI 驱动编程的时代,学会耐心地与大模型进行沟通,通过一次次的对话来完成代码编写工作是很必要的。


Step5:Midscene AI 视觉 API 详解

当传统定位无法胜任时,使用 PlaywrightAgent 的 AI 方法。

import{PlaywrightAgent}from'@midscene/web/playwright';

constagent=newPlaywrightAgent(page);
aiTap(prompt) — 视觉点击
// 找到元素并点击,prompt 用自然语言描述"你想点什么"
awaitagent.aiTap('页面右上角的用户头像');
awaitagent.aiTap('"买牛奶"这条任务右侧的红色删除按钮');
awaitagent.aiTap('弹窗底部的"确认"按钮');

// 精确描述可以减少 AI 推理时间和 Token 消耗
// ❌ 模糊:await agent.aiTap('按钮');
// ✅ 精确:await agent.aiTap('新建任务表单右侧的提交按钮');
aiInput(prompt, { value }) — 视觉输入
// 找到输入框并输入内容
awaitagent.aiInput('任务名称输入框',{value:'买牛奶'});
awaitagent.aiInput('页面顶部的搜索框',{value:'keyword'});

// 适用场景:富文本编辑器(CodeMirror/Quill)、无 name/id 的输入框
awaitagent.aiInput('正文编辑区域(富文本框)',{value:'这是正文内容'});
aiAssert(prompt) — 视觉断言
// 判断页面当前状态是否符合预期
awaitagent.aiAssert('任务列表中存在"买牛奶"这条记录');
awaitagent.aiAssert('页面显示了成功提示 Toast');
awaitagent.aiAssert('提交按钮处于禁用状态(灰色不可点击)');
aiQuery(prompt) — 从页面提取信息
// 从页面视觉提取结构化数据
constresult=awaitagent.aiQuery(
'{ count: number }',
'任务列表当前显示的任务总数'
);
console.log(result.count);// 例如:5

conststatus=awaitagent.aiQuery(
'{ text: string }',
'页面顶部状态栏的文字内容'
);
aiAction(prompt) — 合并多步操作(省 Token)
// 将多个连续操作合并为一次 AI 调用,只截图一次
// 适合填写表单等连续输入场景

// ❌ 低效:3 次截图 + 3 次 AI 调用
awaitagent.aiInput('标题输入框',{value:'我的任务'});
awaitagent.aiInput('描述输入框',{value:'任务描述'});
awaitagent.aiTap('提交按钮');

// ✅ 高效:1 次截图 + 1 次 AI 调用
awaitagent.aiAction(
'在标题输入框填入"我的任务",在描述输入框填入"任务描述",然后点击提交按钮'
);

Step6:一个完整的测试用例文件

把以上所有元素放在一起,看一个完整的可运行示例:

import'./helpers/setup';

import{test,expect}from'@playwright/test';
import{PlaywrightAgent}from'@midscene/web/playwright';

constBASE_URL='http://localhost:3000';

test.describe('Todo 应用',()=>{

test('新建任务 - 传统定位(元素有稳定属性)',async({page})=>{
awaitpage.goto(BASE_URL);

// ✅ placeholder 属性稳定,用传统定位,Token 消耗为 0
awaitpage.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');
awaitpage.locator('button[type="submit"]').click();

// 等待列表刷新
awaitpage.locator('li:has-text("买牛奶")').waitFor({state:'visible'});

awaitexpect(page.locator('li:has-text("买牛奶")')).toBeVisible();
});


test('删除任务 - AI 视觉定位(图标按钮无文案)',async({page})=>{
awaitpage.goto(BASE_URL);

// 先创建任务(传统定位)
awaitpage.locator('input[placeholder="请输入任务名称"]').fill('买牛奶');
awaitpage.locator('button[type="submit"]').click();
awaitpage.locator('li:has-text("买牛奶")').waitFor({state:'visible'});

// ✅ 等待页面稳定,减少 AI 重试
awaitpage.waitForLoadState('networkidle');

// 删除按钮是图标,无文案无 id,用 AI 视觉定位
constagent=newPlaywrightAgent(page);
awaitagent.aiTap('"买牛奶"这条任务右侧的删除按钮');

awaitexpect(page.locator('li:has-text("买牛奶")')).not.toBeVisible();
});


test('标记完成 - AI 视觉断言验证状态',async({page})=>{
awaitpage.goto(BASE_URL);

awaitpage.locator('input[placeholder="请输入任务名称"]').fill('读一本书');
awaitpage.locator('button[type="submit"]').click();
awaitpage.locator('li:has-text("读一本书")').waitFor({state:'visible'});

// 勾选复选框(有 type 属性,传统定位)
awaitpage.locator('li:has-text("读一本书") input[type="checkbox"]').click();

// ✅ 用 AI 断言验证视觉状态(文字是否出现删除线样式)
constagent=newPlaywrightAgent(page);
awaitagent.aiAssert('"读一本书"这条任务显示为已完成状态(文字带删除线或变灰)');
});


test('批量操作 - 用 aiAction 合并多步,节省 Token',async({page})=>{
awaitpage.goto(BASE_URL);

// 一次 AI 调用完成整个表单填写流程
constagent=newPlaywrightAgent(page);
awaitagent.aiAction(
'在任务名称输入框填入"周会准备",'+
'在优先级下拉选择"高",'+
'在备注输入框填入"需要准备 PPT",'+
'然后点击添加按钮'
);

awaitexpect(page.locator('li:has-text("周会准备")')).toBeVisible();
});

});

Step7:运行与查看报告

# 运行所有测试
npx playwright test

# 运行单个文件
npx playwright test tests/todo.spec.ts

# 有头模式运行(可以看到浏览器操作过程)
npx playwright test--headed

# 查看 Midscene 可视化报告(每个 AI 操作都有截图 + 推理结果)
open midscene_run/report/*.html

# 查看 Playwright 测试报告
npx playwright show-report

Midscene 报告示例 — midscene_run/report/ 下的 HTML 报告会展示:

  • 每次 aiTap / aiInput 的截图

  • AI 识别到的目标元素位置(红框标注)

  • 模型的推理过程和置信度

  • 整体执行时间和 Token 消耗

结尾

目前使用的还没有很长时间,感受上还不错,我在另一个项目里也在实践另一套 AI 驱动的方案(基于单模态而非多模态的计算机视觉方案),感受都还不错。 下次会分享另一个方案。

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】

​​

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐