You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/docs/06-测试规范.md

24 KiB

测试规范

版本: 2026-05-03 适用范围: CNC机床数据采集系统 — 前端管理后台 + 后端API


一、历史问题复盘与根因分析

1.1 问题清单(实际发生)

前后端契约断裂5起

# 问题 现象 教训
1 API返回 {value, label},前端绑定 {id, name} 车间/机床/工人下拉框全部无选项 前后端字段名必须对齐,不能各写各的
2 DTO新增 activeMachineCount 等字段Service未填充值 汇总卡片(运行机床/切削总时/平均产量)显示空 DTO加字段 ≠ 数据会自动填上
3 采集状态API返回 IsRunning/LastCollectTime,前端期望 status/uptimeSeconds 采集服务状态不显示 前后端命名风格PascalCase vs camelCase必须统一
4 车间API返回 {data:{items:[...]}} 分页格式 前端取不到选项列表 返回数据结构(数组 vs 分页对象)前后端必须确认
5 修正弹窗HTML模板缺失 弹窗打不开 前端组件完整性检查

测试验证流于形式2起

# 问题 现象 教训
6 Playwright 37项测试全通过但所有功能都不能用 断言只检查"元素存在",不检查"数据内容" 必须验证数据值不能只验证DOM结构
7 没有展开下拉框确认有选项 下拉框无数据但测试"通过" 交互控件必须实际操作验证

后端NULL占位符未填充2起

# 问题 现象 教训
8 SQL直接写 NULL AS TotalRunTime, NULL AS TotalCuttingTime 表格运行时间/切削时间全为空 NULL不报错、不崩溃隐蔽性极强
9 DailySummaryResponse.MachineCount 硬编码为0 运行机床数永远为0 禁止用占位值应付编译

配置/格式硬编码3起

# 问题 现象 教训
10 Element Plus日期格式用 yyyy-MM-dd(错误),应为 YYYY-MM-DDdayjs格式 日期选择器值无法解析 不同库的日期格式不同,必须查阅文档
11 采集服务serviceId为 collector-service,代码硬编码 CncCollector 心跳匹配不上,采集状态显示未启动 关键配置项不能硬编码,必须与实际值一致
12 产量报表默认日期未设为今天 打开页面没有数据 页面初始化必须设置合理的默认值

开发流程缺陷3起

# 问题 现象 教训
13 编译通过 = "测试通过" 功能全部不可用 编译通过 ≠ 功能正确
14 改完代码不发布到IIS就宣称完成 用户看到的是旧版本 每次改动必须发布
15 Playwright只访问页面不操作控件 控件功能全不可用但测试"通过" 必须模拟真实用户操作

1.2 四大根因

根因1前后端接口契约断裂

为什么反复出现: 没有统一的接口契约文档。后端改字段名前端不知道前端加绑定后端没实现。DTO只是C#类型定义,不等于运行时有数据。

改进措施:

  • 每个API端点必须用PowerShell/脚本实际调用确认返回的JSON字段名和结构
  • 前端绑定字段名时必须对照API实际返回值不能凭假设
  • 新增DTO字段后必须追踪到Service层确认有填充逻辑

根因2测试断言停留在DOM结构层面

为什么反复出现: Playwright的 toBeVisible() 只检查元素是否渲染不检查内容。元素渲染了但内容是空的、NULL的、占位符的测试照样通过。

改进措施:

  • 断言必须检查具体文本内容(toHaveText()textContent()
  • 数值断言必须排除空字符串、---undefinednull0(除非业务确认)
  • 下拉框必须展开确认有选项不能只检查select元素存在

根因3NULL占位符滥用

为什么反复出现: 开发时用 NULL AS 字段名 占位编译不报错、API返回200、不崩溃。NULL是"合法的空值",不会被任何自动检查发现。

改进措施:

  • 禁止SQL中写 NULL AS 字段名 作为占位符
  • 如果字段暂时无法计算,应抛出 NotImplementedException 而非返回NULL
  • Code Review时重点检查SELECT列表中的NULL

根因4缺少强制验证门禁

为什么反复出现: 开发完成后没有"必须通过"的验证步骤。编译成功就认为完成了,没有实际操作验证。

改进措施:

  • 建立4层验证流程编译→API→浏览器→发布每层必须通过才能进入下一层
  • Playwright测试脚本覆盖所有交互控件和数据展示
  • 每次改动后立即验证,不累积到"全部改完"

二、测试原则6条铁律

铁律1接口先验证

为什么: 后端改了API返回格式前端不知道就白改。先验证API再写前端避免返工。

怎么做:

  • 改了任何API端点立即用PowerShell脚本调用
  • 检查返回的JSON字段名、字段值、数据结构、非空字段
  • 把实际返回值和前端绑定对比,逐字段确认一致
# 登录获取token
$loginRes = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/login" -Method Post -ContentType "application/json" -Body '{"username":"admin","password":"admin123"}'
$token = $loginRes.data.token
$headers = @{ Authorization = "Bearer $token" }

# 调用目标API
$result = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/production/daily-summary?date=2026-05-03" -Headers $headers
$result.data | ConvertTo-Json  # 逐字段检查

铁律2数据断言必须具体

为什么: toBeVisible() 只能证明元素渲染了,不能证明数据正确。空值、占位符、错误值都能通过。

怎么做:

  • 禁止只检查元素存在
  • 必须检查元素的文本内容
  • 数值必须是非空、非占位符的具体数字
// ❌ 错误:只检查元素存在
await expect(page.locator('.card-value')).toBeVisible()

// ✅ 正确:检查具体数值
const value = await page.locator('.card-value').textContent()
expect(value).toBeTruthy()
expect(value).not.toBe('')
expect(value).not.toBe('-')
expect(value).not.toBe('--')

铁律3下拉框必须展开验证

为什么: 下拉框渲染了不代表有选项。API返回空数组时select元素照样显示只是展开后没有选项。

怎么做:

  1. 点击下拉框展开
  2. 等待下拉选项出现
  3. 断言选项数量 >= 1
  4. 选择一个选项
  5. 确认输入框显示选中值
  6. 点查询,确认筛选生效
// ❌ 错误只检查select元素存在
await expect(page.locator('.el-select')).toBeVisible()

// ✅ 正确:展开→确认有选项→选择→验证
await page.locator('.el-select').click()  // 点击外层wrapper展开
const options = page.locator('.el-select-dropdown__item:visible')
await expect(options.first()).toBeVisible()
const count = await options.count()
expect(count).toBeGreaterThan(0)
await options.first().click()
await page.getByRole('button', { name: '查询' }).click()

铁律4表格必须检查数据列

为什么: 表格渲染了不代表有数据。后端返回空数组时表格只显示表头,但"表格可见"的断言照样通过。

怎么做:

  • 检查表格有数据行(.el-table__row 数量 > 0
  • 取第一行,逐列检查文本内容不为空
const rows = page.locator('.el-table__body-wrapper .el-table__row')
const rowCount = await rows.count()
expect(rowCount).toBeGreaterThan(0)

// 检查第一行的关键列
const firstRow = rows.first()
const cells = firstRow.locator('td .cell')
const dateText = await cells.nth(0).textContent()
expect(dateText).toBeTruthy()
expect(dateText).not.toBe('')

铁律5卡片/统计必须检查数值

为什么: 卡片渲染了但数值是空、0、undefined、NaN用户看到的是空白或错误。

怎么做:

  • 每个卡片取数值部分
  • 断言不为空字符串、不为 -、不为 --、不为 undefined、不为 null
  • 如果业务上允许为0需要明确注释说明

铁律6改完即验证不等批量

为什么: 改了10处再一起验证出问题时无法定位是哪一处改坏的。而且累积到后面容易漏验证。

怎么做:

  • 每改一处 → 编译 → API验证 → 浏览器验证
  • 全部通过后再改下一处
  • 如果改动互相关联最多合并2-3处一起验证

三、验证层级4层

L1 编译验证

执行时机: 每次代码改动后 命令:

dotnet build src\CncWebApi\CncWebApi.csproj   # 0错误
cd frontend && npm run build                    # 0错误

通过标准: 两个命令都返回退出码0 注意: 编译通过 ≠ 功能正确,这只是最基本的大门

L2 API验证

执行时机: 改了后端代码后 方法: 用PowerShell调用改动过的API端点 检查项:

  • HTTP状态码200
  • 返回JSON的 code 字段为0
  • 返回 data 不为null
  • 关键字段值不为空/NULL
  • 字段名和前端绑定一致

PowerShell模板

# 登录
$loginRes = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/login" -Method Post -ContentType "application/json" -Body '{"username":"admin","password":"admin123"}'
$token = $loginRes.data.token
$headers = @{ Authorization = "Bearer $token" }
$today = Get-Date -Format "yyyy-MM-dd"

# 示例:验证产量汇总
$sum = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/production/daily-summary?date=$today" -Headers $headers
echo "总产量: $($sum.data.totalQuantity)"        # 应有值
echo "运行机床: $($sum.data.activeMachineCount)"  # 应 > 0
echo "切削总时: $($sum.data.totalCuttingTime)"    # 应有值
echo "平均产量: $($sum.data.avgQuantityPerMachine)" # 应有值

# 示例:验证下拉选项
$ws = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/workshop/list" -Headers $headers
echo "车间数量: $($ws.data.items.Count)"          # 应 > 0

L3 浏览器验证

执行时机: 改了前端代码后、或需要验证用户交互时 方法: Playwright打开页面实际操作每个交互控件 检查项: 见第五节"Playwright E2E测试断言标准"

运行命令:

cd frontend
npx playwright test e2e/smoke-iis.spec.ts --project=chromium

L4 发布验证

执行时机: 所有改动完成后、正式交付前 步骤:

  1. 回收AppPoolappcmd recycle apppool "haoliang"
  2. 重复L2API验证
  3. 重复L3浏览器验证
  4. 确认 http://192.168.1.202/admin/ 局域网可访问

四、前后端接口契约验证

4.1 字段名对齐检查

每次新增或修改API返回字段时必须执行以下检查

检查项 方法 通过标准
后端DTO字段名 vs API实际返回JSON PowerShell调用API查看返回值 JSON key名与DTO属性名一致考虑camelCase序列化
API返回字段名 vs 前端绑定 对比Vue模板中的 prop/:key/:label/:value 完全一致或兼容(支持 ?? 回退)
返回数据结构 查看JSON嵌套层级 前端取值路径正确(res.data.items vs res.data
空值处理 构造空数据场景 前端显示合理的占位符(-暂无数据),不报错

4.2 常见陷阱

  • Newtonsoft.Json camelCase序列化C#的 TotalQuantity → JSON的 totalQuantity,前端绑定要对应
  • 分页格式部分API返回数组 [{...}],部分返回分页对象 {items:[], total:0},前端必须区分
  • 枚举值:后端枚举可能序列化为整数或字符串,前端必须处理两种情况
  • DateTime序列化:可能为 "2026-05-03T00:00:00""2026-05-03",前端日期解析要兼容

五、Playwright E2E测试断言标准

强制要求每次编写Playwright测试时必须逐项对照以下维度清单。每个页面的每个维度都必须有对应测试用例覆盖不允许遗漏。

5.0 测试维度总表(每个页面必查)

以下20个维度是每个管理后台页面的最低测试覆盖要求。写任何页面的Playwright测试时必须逐条确认是否有对应测试用例。如果某个维度不适用于当前页面如该页面无下拉框需在测试文件中用注释说明跳过原因。

# 维度 必须断言的内容 适用场景 禁止做法
1 页面加载 URL正确 + 关键元素可见 所有页面 只goto不等待
2 默认数据 页面打开即有数据(表格行数>0 / 卡片有值) 有数据展示的页面 打开页面不检查数据
3 汇总卡片数值 每个卡片的数值 ≠ 空 ≠ ---undefinednull 有统计卡片的页面 只检查卡片可见不检查数值
4 表格行数 .el-table__row 数量 > 0 有表格的页面 只检查table可见
5 表格每列有值 第一行每列文本 ≠ 空(日期、名称、数值、时间、状态等逐列检查) 有表格的页面 只检查前2-3列
6 运行时间/数值列 时间列有具体数值非空非NULL 有时间/数值列的页面 跳过时间列验证
7 状态标签 标签文本为已知中文文案(正常/离线/缺失/告警等) 有状态列的页面 不检查状态文本内容
8 下拉框展开 点击展开 → 选项数量 ≥ 1 → 每个选项文本非空非占位 有下拉框的页面 只检查select可见
9 下拉框选择 选择一项 → 输入框显示选中label 有下拉框的页面 选择后不验证显示值
10 筛选生效 选择筛选条件 → 点查询 → 表格数据变化 有查询功能的页面 不验证筛选后数据是否真的变了
11 文本筛选 输入关键词 → 点查询 → 结果每行包含关键词 有文本搜索框的页面 不验证搜索结果匹配度
12 日期筛选 切换日期范围 → 查询 → 数据正确变化 有日期选择的页面 不验证日期筛选效果
13 重置按钮 重置后所有筛选恢复默认占位符 有重置按钮的页面 不验证重置是否清空
14 分页切换 切到第2页 → 页码高亮 + 表格第一行数据变化 有分页且数据>1页 只检查分页组件可见
15 弹窗打开 点击触发 → 弹窗可见 + 标题正确 + 表单有初始值 有弹窗的页面 不验证弹窗标题和内容
16 弹窗提交闭环 填写表单 → 提交 → 成功提示 → 列表刷新 → 数据已变更 有写操作弹窗的页面 只验证弹窗能打开不验证提交
17 API vs 页面对账 调API拿JSON → 对比页面卡片/表格显示值是否一致 有统计卡片的页面 不和API实际值对比
18 下拉框选项值有效性 每个选项的文本非空、value可选中 有下拉框的页面 不验证选项文本内容
19 空数据状态 选不可能的条件 → 表格行数为0 + 无"白屏/报错" 有筛选功能的页面 不验证极端情况
20 错误处理 异常场景(如网络断开)→ 显示友好提示不白屏 所有页面 不考虑异常场景

5.1 维度适用性速查表

不同类型的页面,必须覆盖的维度子集不同。写测试前先确认页面类型:

页面类型 必须覆盖的维度编号
列表页(带表格+筛选) 1,2,4,5,8,9,10,13,14,19
列表页(带统计卡片) 1,2,3,4,5,8,9,10,13,14,17,19
仪表盘/概览页 1,2,3
详情/表单页 1,15,16
所有页面通用 1,20

5.3 交互控件测试矩阵

每个页面的每个交互控件,都必须按此矩阵验证:

控件类型 验证动作 通过标准 禁止做法
下拉框 1.点击展开 2.等待选项渲染 选项数量 ≥ 1 只检查select元素可见
下拉框 选择某项 输入框文本变为选中项的label 不验证选择后的显示值
下拉框 选择后点查询 表格数据变化(筛选生效) 不验证筛选是否实际生效
日期选择器 打开选择日期 输入框显示 YYYY-MM-DD 格式 不验证日期格式
查询按钮 点击 表格刷新loading出现或数据变化 只检查按钮可见
重置按钮 点击 所有筛选恢复默认值 不验证筛选是否清空
分页 切换页码 表格数据变化,页码高亮正确 只检查分页组件可见
弹窗-打开 点击触发按钮 弹窗可见,标题正确 不验证弹窗标题
弹窗-表单 查看表单字段 各字段有合理初始值 不检查表单初始值
弹窗-提交 填写并提交 返回成功提示,列表刷新 不验证提交后的状态
导出 点击导出 触发下载或显示提示 不验证导出行为

5.4 数据展示验证矩阵

展示元素 验证标准 非法值(不通过)
汇总卡片 每个数值:非空 ∧ ≠ "" ∧ ≠ "-" ∧ ≠ "--" ∧ ≠ "undefined" ∧ ≠ "null" 空字符串、---undefinednull
表格数据列 每列至少第一行有实际文本值 空单元格、NULL
状态标签 显示正确的中文文案(正常/离线/缺失/告警等) 空标签、英文文本
时间列 有值且格式正确(YYYY-MM-DDHH:mm:ss 空值、格式错误
空状态 无数据时显示"暂无数据"或类似提示 空白页面、报错

5.5 断言红黑榜

正确的断言(数据层面)

// 检查文本不为空且非占位符
const text = await locator.textContent()
expect(text).toBeTruthy()
expect(text!.trim()).not.toBe('')
expect(text!.trim()).not.toBe('-')
expect(text!.trim()).not.toBe('--')

// 检查下拉框有选项
const options = page.locator('.el-select-dropdown__item')
await expect(options.first()).toBeVisible()
const count = await options.count()
expect(count).toBeGreaterThan(0)

// 检查表格有数据行
const rows = page.locator('.el-table__body-wrapper .el-table__row')
expect(await rows.count()).toBeGreaterThan(0)

// 检查卡片数值
const cardValue = await page.locator('.card-value').textContent()
expect(parseFloat(cardValue!)).not.toBeNaN()

错误的断言(结构层面,太宽松)

// 这些断言无法发现数据为空的问题
await expect(page.locator('.card')).toBeVisible()        // 卡片可见但值可能为空
await expect(page.locator('table')).toBeVisible()        // 表格可见但可能没数据
await expect(page.locator('select')).toBeVisible()       // 下拉框可见但可能没选项
await expect(page.locator('.el-dialog')).toBeVisible()   // 弹窗可见但内容可能错误

六、API验证方法

6.1 PowerShell验证脚本模板

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# 登录获取Token
$loginRes = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/login" -Method Post -ContentType "application/json" -Body '{"username":"admin","password":"admin123"}'
$token = $loginRes.data.token
$headers = @{ Authorization = "Bearer $token" }
$today = Get-Date -Format "yyyy-MM-dd"

# === 产量模块 ===
echo "--- 产量汇总 ---"
$sum = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/production/daily-summary?date=$today" -Headers $headers
echo "总产量: $($sum.data.totalQuantity)(应有值)"
echo "运行机床: $($sum.data.activeMachineCount)(应>0"
echo "切削总时: $($sum.data.totalCuttingTime)(应有值)"
echo "平均产量: $($sum.data.avgQuantityPerMachine)(应有值)"

echo "--- 产量列表前3条 ---"
$prod = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/production/daily?startDate=$today&endDate=$today&page=1&pageSize=3" -Headers $headers
echo "总数: $($prod.data.total)"
foreach ($item in $prod.data.items) {
    echo "  机床=$($item.machineName) 产量=$($item.totalQuantity) 运行=$($item.totalRunTime)h 切削=$($item.totalCuttingTime)h"
}

# === 下拉选项 ===
echo "--- 车间选项 ---"
$ws = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/workshop/list" -Headers $headers
echo "数量: $($ws.data.items.Count)(应>0"

echo "--- 机床选项 ---"
$mc = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/machine/list" -Headers $headers
echo "数量: $($mc.data.items.Count)(应>0"

echo "--- 工人选项 ---"
$wr = Invoke-RestMethod -Uri "http://127.0.0.1/api/admin/worker/list" -Headers $headers
echo "数量: $($wr.data.items.Count)(应>0"

6.2 非空字段验证清单

每个API端点返回后以下字段必须非空

API端点 必须非空字段
/admin/production/daily-summary totalQuantity, activeMachineCount, totalCuttingTime, avgQuantityPerMachine
/admin/production/daily items[].machineName, items[].totalQuantity, items[].date
/admin/workshop/list items[].value, items[].label
/admin/machine/list items[].value, items[].label
/admin/worker/list items[].value, items[].label
/admin/dashboard/statistics 各统计卡片字段
/admin/collector/status status

七、发布前Checklist强制全部打勾

每次交付前必须完成以下所有检查项:

编译

  • dotnet build src\CncWebApi\CncWebApi.csproj → 0错误
  • cd frontend && npm run build → 0错误

发布

  • 前端build输出已复制到 src\CncWebApi\admin\
  • 回收AppPoolappcmd recycle apppool "haoliang"

API验证

  • 改动过的API端点用PowerShell调用返回值正确
  • 新增/修改的字段有实际值非NULL、非空

浏览器验证

  • Playwright冒烟测试通过cd frontend && npx playwright test e2e/smoke-iis.spec.ts --project=chromium
  • 下拉框展开有选项、选择后筛选生效
  • 表格数据列有值
  • 汇总卡片数值正确
  • 弹窗能打开且有内容

Git

  • git add + git commit -m "中文描述" + git push

八、反模式清单(禁止做法)

# 反模式 正确做法
1 编译通过 = 测试通过 编译通过只说明语法正确,必须验证功能
2 只检查 toBeVisible() 不检查内容 必须检查文本值,排除空/占位符
3 SQL写 NULL AS 字段名 占位 禁止占位,要么实现要么抛异常
4 DTO加字段不填充值 加字段必须追踪到Service层实现填充
5 改完代码不发布到IIS 每次改动必须发布后验证
6 Playwright不操作控件只看页面 必须模拟用户操作:点击、选择、输入
7 累积改动到"全部完成"再验证 改完一处立即验证
8 前端凭假设绑定字段名 必须对照API实际返回值
9 硬编码配置值serviceId、端口等 从配置文件读取或与实际部署保持一致
10 不检查Element Plus组件库的日期/格式要求 查阅文档确认格式(如 dayjs 的 YYYY-MM-DD
11 Playwright测试不按维度表逐项覆盖 必须对照5.0节维度总表,每个维度有测试用例或注释跳过原因
12 测试脚本只覆盖部分页面 每次改动涉及的页面都必须有测试覆盖
13 新增测试维度后不更新维度总表 维度总表是强制标准,任何新发现的问题必须补充为新维度