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

460 lines
19 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 测试规范
**版本:** 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-DD`dayjs格式 | 日期选择器值无法解析 | 不同库的日期格式不同,必须查阅文档 |
| 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()`
- 数值断言必须排除空字符串、`-`、`--`、`undefined`、`null`、`0`(除非业务确认)
- 下拉框必须展开确认有选项不能只检查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字段名、字段值、数据结构、非空字段
- 把实际返回值和前端绑定对比,逐字段确认一致
```powershell
# 登录获取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()` 只能证明元素渲染了,不能证明数据正确。空值、占位符、错误值都能通过。
**怎么做:**
- 禁止只检查元素存在
- 必须检查元素的文本内容
- 数值必须是非空、非占位符的具体数字
```typescript
// ❌ 错误:只检查元素存在
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. 点查询,确认筛选生效
```typescript
// ❌ 错误只检查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
- 取第一行,逐列检查文本内容不为空
```typescript
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 编译验证
**执行时机:** 每次代码改动后
**命令:**
```bash
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模板**
```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测试断言标准"
**运行命令:**
```bash
cd frontend
npx playwright test e2e/smoke-iis.spec.ts --project=chromium
```
### L4 发布验证
**执行时机:** 所有改动完成后、正式交付前
**步骤:**
1. 回收AppPool`appcmd 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测试断言标准
### 5.1 交互控件测试矩阵
每个页面的每个交互控件,都必须按此矩阵验证:
| 控件类型 | 验证动作 | 通过标准 | 禁止做法 |
|----------|----------|----------|----------|
| **下拉框** | 1.点击展开 2.等待选项渲染 | 选项数量 ≥ 1 | 只检查select元素可见 |
| **下拉框** | 选择某项 | 输入框文本变为选中项的label | 不验证选择后的显示值 |
| **下拉框** | 选择后点查询 | 表格数据变化(筛选生效) | 不验证筛选是否实际生效 |
| **日期选择器** | 打开选择日期 | 输入框显示 `YYYY-MM-DD` 格式 | 不验证日期格式 |
| **查询按钮** | 点击 | 表格刷新loading出现或数据变化 | 只检查按钮可见 |
| **重置按钮** | 点击 | 所有筛选恢复默认值 | 不验证筛选是否清空 |
| **分页** | 切换页码 | 表格数据变化,页码高亮正确 | 只检查分页组件可见 |
| **弹窗-打开** | 点击触发按钮 | 弹窗可见,标题正确 | 不验证弹窗标题 |
| **弹窗-表单** | 查看表单字段 | 各字段有合理初始值 | 不检查表单初始值 |
| **弹窗-提交** | 填写并提交 | 返回成功提示,列表刷新 | 不验证提交后的状态 |
| **导出** | 点击导出 | 触发下载或显示提示 | 不验证导出行为 |
### 5.2 数据展示验证矩阵
| 展示元素 | 验证标准 | 非法值(不通过) |
|----------|----------|------------------|
| **汇总卡片** | 每个数值:非空 ∧ ≠ `""` ∧ ≠ `"-"` ∧ ≠ `"--"` ∧ ≠ `"undefined"` ∧ ≠ `"null"` | 空字符串、`-`、`--`、`undefined`、`null` |
| **表格数据列** | 每列至少第一行有实际文本值 | 空单元格、`NULL` |
| **状态标签** | 显示正确的中文文案(正常/离线/缺失/告警等) | 空标签、英文文本 |
| **时间列** | 有值且格式正确(`YYYY-MM-DD` 或 `HH:mm:ss` | 空值、格式错误 |
| **空状态** | 无数据时显示"暂无数据"或类似提示 | 空白页面、报错 |
### 5.3 断言红黑榜
#### ✅ 正确的断言(数据层面)
```typescript
// 检查文本不为空且非占位符
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()
```
#### ❌ 错误的断言(结构层面,太宽松)
```typescript
// 这些断言无法发现数据为空的问题
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验证脚本模板
```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\`
- [ ] 回收AppPool`appcmd 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` |