Files
xiaomihome/miot/TEST_PLAN.md
T
4566704 3a44cb9e6a feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写)
- 支持云端 API 调用与局域网设备发现(mDNS)
- 集成国际化(i18n)多语言支持
- 添加 MIoT 设备规约解析器(spec_parser)
- 包含单元测试与使用示例
2026-06-28 22:05:49 +08:00

521 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# go-miot 单元测试计划
> 状态: 待执行 | 创建: 2026-06-28 | 目标: 每个源文件一个测试文件,覆盖率 > 80%
---
## 目录结构约定
所有测试文件与源文件**同目录**,命名规则: `{源文件名}_test.go`
| 层次 | 包名策略 | 原因 |
|------|----------|------|
| 纯逻辑/工具函数 | `package miot_test` (黑盒) | 只测公开API即可 |
| 客户端层(需访问内部锁/定时器) | `package miot` (白盒) | 需访问私有字段 |
现有 `miot_test.go` 稍后拆分为各独立 `_test.go` 文件。
---
## Batch 1: 纯函数层 (无外部依赖) 🔴 P0
**启动命令**: 写完就能跑 `go test -v -run "TestCtrlMode|TestError|TestCommon"`
### 1.1 `const_test.go` — 常量与枚举
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | 无 |
| 难度 | ⭐ |
| # | 函数/值 | 测试内容 |
|---|---------|---------|
| 1 | `CtrlMode.String()` | cloud/gateway/lan 三值 + unknown |
| 2 | `SUPPORTED_PLATFORMS` | 长度 > 0,包含 "switch", "light" 等关键值 |
| 3 | `UNSUPPORTED_MODELS` | 长度验证 |
| 4 | `CLOUD_SERVERS` | 包含 "cn", "us" 等 key |
| 5 | `INTEGRATION_LANGUAGES` | 包含 "zh-Hans", "en" |
| 6 | `MIHOME_CA_CERT_STR` | 非空,以 `-----BEGIN CERTIFICATE-----` 开头 |
| 7 | `MIHOME_CA_CERT_SHA256` | 长度 = 64 (SHA256 hex) |
| 8 | `RefreshPropsDelay` | = 200 |
| 9 | `RefreshPropsRetryMax` | = 3 |
| 10 | `TopicProp / TopicEvent / TopicState` | = "properties_changed" / "event_occured" / "state" |
---
### 1.2 `miot_error_test.go` — 错误类型
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | 无 |
| 难度 | ⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `NewMIoTOauthError(msg)` | 返回 `*MIoTOauthError`,Code=`CodeOauthUnauthorized`,Message=msg |
| 2 | `NewMIoTHttpError(msg)` | 返回 `*MIoTHttpError`,Code=`CodeHttpInvalidAccessToken` |
| 3 | `NewMIoTMipsError(msg)` | 返回 `*MIoTMipsError`,Code=`CodeMipsInvalidResult` |
| 4 | `NewMIoTDeviceError(msg)` | 返回 `*MIoTDeviceError`,Code=`CodeUnknown` |
| 5 | `NewMIoTSpecError(msg)` | 返回 `*MIoTSpecError` |
| 6 | `NewMIoTStorageError(msg)` | 返回 `*MIoTStorageError` |
| 7 | `NewMIoTCertError(msg)` | 返回 `*MIoTCertError`,Code=`CodeCertInvalidCert` |
| 8 | `NewMIoTClientError(msg)` | 返回 `*MIoTClientError` |
| 9 | `NewMIoTEvError(msg)` | 返回 `*MIoTEvError` |
| 10 | `NewMipsServiceError(msg)` | 返回 `*MipsServiceError` |
| 11 | `NewMIoTConfigError(msg)` | 返回 `*MIoTConfigError`,Code=`CodeConfigInvalidInput` |
| 12 | `NewMIoTOptionsError(msg)` | 返回 `*MIoTOptionsError` |
| 13 | `NewMIoTLanError(msg)` | 返回 `*MIoTLanError`,Code=`CodeLanUnavailable` |
| 14 | `(*MIoTError).Error()` | 格式: `"code={code}, message={msg}"` |
| 15 | `(*MIoTError).ToDict()` | 返回 `map`,含 "code" 和 "message" key |
> 建议: 用 table-driven test, 12 个构造函数一个循环测完。
---
### 1.3 `common_test.go` — 工具函数
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | 文件系统 (`os.ReadFile`) |
| 难度 | ⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `CalcGroupID(uid, homeID)` | 相同输入→相同输出;长度=16;hex字符串 |
| 2 | `GenAbsolutePath(path)` | 拼接正确;相对路径→绝对路径 |
| 3 | `LoadJSONFile(path)` | 有效JSON→正确解析;不存在文件→error;无效JSON→error |
| 4 | `LoadYAMLFile(path)` | 有效YAML→正确解析;不存在文件→error |
| 5 | `RandomizeInt(value, ratio)` | 结果在 `[value*(1-ratio), value*(1+ratio)]` 范围内 |
| 6 | `RandomizeFloat(value, ratio)` | 结果在有效范围内 |
| 7 | `SlugifyName(name)` | "Hello World" → "hello_world";特殊字符处理;边界:空字符串 |
| 8 | `SlugifyDID(server, did)` | = `SlugifyName(server + "_" + did)` |
---
## Batch 2: 数据结构层 (轻依赖) 🔴 P1
### 2.1 `miot_matcher_test.go` — 订阅匹配树 (补充增强)
| 包名 | `package miot_test` (黑盒) |
|------|-----|
| 依赖 | 无 |
| 难度 | ⭐⭐ |
现有测试已覆盖基本 Sub/Match/Unsub,需要补充:
| # | 测试项 | 说明 |
|---|--------|------|
| 1 | `#` 通配符应匹配且是最后一段 | `did1/p/#` 应匹配 `did1/p/2/1` |
| 2 | `#` 不在最后一段→不匹配 | `did1/#/1` 不应匹配 `did1/p/2/1` |
| 3 | `+` 单层通配符 | `did1/+/2/1` 应匹配 `did1/p/2/1`,不匹配 `did1/p/x/2/1` |
| 4 | 完全不匹配 topic | `did2/p/2/1` 不应匹配 `did1/p/2/1` |
| 5 | `Clear()` 清空订阅 | Clear后 Size()=0,Match返回空 |
| 6 | `Size()` 计数器 | 多次 Sub 后 Size() 正确 |
| 7 | 同一个 handler 多次 Sub | 不重复计数 |
| 8 | 并发 Sub/Unsub/Match | `-race` 检测无 data race |
| 9 | Unsub 不存在的 ID | 不应 panic |
---
### 2.2 `miot_storage_test.go` — 文件存储
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | 文件系统 (临时目录) |
| 难度 | ⭐⭐⭐ |
使用 `os.MkdirTemp` 创建临时目录,`defer os.RemoveAll` 清理。
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `NewMIoTStorage(path)` | 目录不存在时自动创建 |
| 2 | `Save(domain, name, data)` + `Load()` | 保存 string→读取正确;dict类型→正确解析 |
| 3 | `Save` nil data → error | 返回 `*MIoTStorageError` |
| 4 | `Load` 文件不存在 → (nil, nil) | 不返回 error |
| 5 | 数据完整性校验 | 修改文件尾部hash→Load返回nil(校验失败) |
| 6 | `Remove(domain, name, typeName)` | 删除后 Load 返回 nil |
| 7 | `RemoveDomain(domain)` | 递归删除整个目录 |
| 8 | `FileExists(domain, name)` | 存在→true,不存在→false |
| 9 | `GetNames(domain, typeName)` | 正确过滤后缀 |
| 10 | `SaveFile / LoadFile` | 无 hash 的原始文件读写 |
| 11 | `Clear()` | 清空所有文件,根目录保留 |
| 12 | `GenStoragePath(domain, name)` | 路径拼接正确 |
| 13 | `LoadAsync / SaveAsync` | channel 返回正确结果 |
| 14 | `UpdateUserConfig` | 新建、合并、replace、删除(config=nil) |
| 15 | `LoadUserConfig` | keys=nil→全部;keys=空切片→空map;指定keys→过滤 |
| 16 | `NewMIoTCert` | storage=nil→error;uid为空→error |
| 17 | `MIoTCert.CAFile/KeyFile/CertFile()` | 路径正确 |
| 18 | `VerifyCACert()` | 首次运行写入嵌入CA→验证通过 |
| 19 | `SaveUserKey/LoadUserKey` | 保存→读取一致 |
| 20 | `SaveUserCert/LoadUserCert` | 保存→读取一致 |
| 21 | `DidHash(did)` | 稳定输出;相同输入→相同hash |
| 22 | `DeviceManufacturer` | Init/DeInit/GetName/SaveManufacturerData |
| 23 | `encodeToBytes / decodeByType` | bytes/str/dict/list 四种类型往返测试 |
---
### 2.3 `miot_i18n_test.go` — 国际化翻译
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | 文件系统 (`i18n/` 目录下的JSON) |
| 难度 | ⭐⭐ |
| # | 测试项 | 说明 |
|---|--------|------|
| 1 | `NewMIoTI18n("en")` | 创建实例 |
| 2 | `Init()` 加载 valid JSON | data 非空 |
| 3 | `Init()` 文件不存在 | data 保持空,不 panic |
| 4 | `Translate` 嵌套 key | `"device.light.name"` → 遍历嵌套 |
| 5 | `Translate` key 不存在 → "" | 不 panic |
| 6 | `Translate` + replace map | `{key}` 替换为 value |
| 7 | `Deinit()` | data 清空 |
| 8 | `Translate` with non-string value | 返回 "" |
---
## Batch 3: 业务逻辑层 (中等复杂度) 🟡 P2
### 3.1 `miot_device_test.go` — 设备模型
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | 需 `*MIoTClient` (可为 nil 测大部分) |
| 难度 | ⭐⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `NewMIoTDevice(client, info, spec)` | 从 map 解析所有字段: did/name/model/online/home_id/... |
| 2 | `NewMIoTDevice` model 含 "." → `modelStrs` 拆分 | `"lumi.plug.v1"` → `["lumi","plug","v1"]` |
| 3 | `NewMIoTDevice` float64 类型字段 | connectType, rssi 从 float64 转换 |
| 4 | `LoadFromCloud(client, cloudData)` | = NewMIoTDevice(client, cloudData, nil) |
| 5 | `LoadSPEC(specInstance)` | nil→error;有效→serviceList 非空 |
| 6 | 所有 getter: `DID()/Name()/Model()/Online()...` | 返回正确值 |
| 7 | `SetOnline(bool)` | getter 返回更新值 |
| 8 | `SetSpecInstance(spec)` | `SpecInstance()` 返回更新值 |
| 9 | `HasWiFi()` | connectType != -1 → true |
| 10 | `ToDict()` | 序列化正确;sub_devices 递归序列化 |
| 11 | `FromDict(client, data)` | 反序列化恢复所有字段 |
| 12 | `AddSubDevice / GetSubDevice` | 添加→读取;不存在→nil |
| 13 | `SubscribeState / UnsubscribeState` | 注册→通知 |
| 14 | `NotifyStateChange(state)` | 触发所有订阅者 |
| 15 | `SubProp / UnsubProp` | client=nil→返回"";正常→返回subID |
| 16 | `SubEvent / UnsubEvent` | 同 SubProp |
---
### 3.2 `spec_parser_test.go` — SPEC 解析
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | 文件系统 (测试JSON spec文件), 网络 (Refresh 阶段) |
| 难度 | ⭐⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `NewSpecStdLib(lang)` | lang="" → default "en" |
| 2 | `SpecStdLib.Load(data)` | 加载所有 6 种 map |
| 3 | `SpecStdLib.Dump()` | Load→Dump 往返一致 |
| 4 | `SpecStdLib.DeviceTranslate(key)` | 命中→翻译;不命中→"" |
| 5 | `SpecStdLib.ServiceTranslate` | 同上 |
| 6 | `SpecStdLib.PropertyTranslate` | 同上 |
| 7 | `SpecStdLib.EventTranslate` | 同上 |
| 8 | `SpecStdLib.ActionTranslate` | 同上 |
| 9 | `SpecStdLib.ValueTranslate` | 同上 |
| 10 | `SpecHash(typeStr, iids...)` | 稳定输出 |
| 11 | `SpecFormatGoType(format)` | "string"→"str", "bool"→"bool", "float"→"float", other→"int" |
| 12 | `SpecPrecision(step)` | 0→0, 0.1→1, 0.01→2 |
| 13 | `parseAccess(interface{})` | []interface{}→[]string; string→[string]; nil→nil |
| 14 | `parseValueRange(interface{})` | dict格式→struct; list格式→struct; nil→nil |
| 15 | `parseValueList(interface{}, ...)` | 正常解析;重复描述去重 |
| 16 | `strVal(m, key)` | 存在→string; 不存在→""; 非string→fmt.Sprintf |
| 17 | `intVal(m, key)` | float64/int/string/json.Number 四种输入 |
| 18 | `boolVal(m, key)` | bool/string("true"/"1") |
| 19 | `NewMIoTSpecParser(lang, storage, dir)` | 参数正确赋值 |
| 20 | `MIoTSpecParser.skipProprietary(item)` | proprietary=true→true; 不存在→false |
| 21 | `MIoTSpecParser.readSpecFile(urn)` | 有效URN→解析; 无效→error |
| 22 | `MIoTSpecParser.parseSpec(urn, model, data)` | 完整解析返回 MIoTSpecInstance |
| 23 | `MIoTSpecParser.parseService` | device-information 类型跳过 |
| 24 | `MIoTSpecParser.parseProperty` | type/description/format/access 任一缺失→nil |
| 25 | `MIoTSpecParser.parseEvent` | type/description 任一缺失→nil |
| 26 | `MIoTSpecParser.parseAction` | type/description 任一缺失→nil |
---
### 3.3 `web_pages_test.go` — 页面模板
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | embed 模板 |
| 难度 | ⭐ |
| # | 测试项 | 说明 |
|---|--------|------|
| 1 | `OAuthRedirectPage` success=true | STATUS_PLACEHOLDER → "true" |
| 2 | `OAuthRedirectPage` success=false | STATUS_PLACEHOLDER → "false" |
| 3 | `OAuthRedirectPage` placeholder 替换 | TITLE/CONTENT/BUTTON 正确替换 |
| 4 | HTML 不包含占位符 | 所有 PLACEHOLDER 被替换 |
| 5 | 边界: 特殊字符 title | 不破坏 HTML 结构 |
---
## Batch 4: 客户端核心层 (需 Mock) 🟡 P3
### 4.1 `miot_client_test.go` — 客户端核心 (拆分自旧的 miot_test.go)
| 包名 | `package miot` (白盒) |
|------|-----|
| 依赖 | MIoTStorage, MIoTHttpClient, MIoTMatcher |
| 难度 | ⭐⭐⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `NewMIoTClient(entryID, data, uid, server, mode)` | 所有字段初始化正确 |
| 2 | `SetStorage / SetHTTPClient / SetMipsCloud / SetMipsLocal` | 设置后可通过内部字段验证 |
| 3 | `Init()` | entryData含 clientID+accessToken→http初始化 |
| 4 | `Init()` | entryData不含凭证→http 为 nil |
| 5 | `Deinit()` | timer 停止; subTree 清空 |
| 6 | `loadCacheDevice()` | storage=nil→error |
| 7 | `DeviceList()` | 返回拷贝,修改不影响内部 |
| 8 | `Start()` | Init + ScheduleRefreshDevices |
| 9 | `Stop()` | = Deinit() |
---
### 4.2 `miot_client_device_test.go` — 设备管理
| 包名 | `package miot` |
|------|-----|
| 难度 | ⭐⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `GetDeviceInfo(did)` | 存在→非nil;不存在→nil |
| 2 | `GetDeviceInfos()` | 返回拷贝 |
| 3 | `LoadDevices()` | loadCacheDevice + RefreshDevices |
| 4 | `RefreshDevices()` | http=nil→error |
| 5 | `updateDeviceList(deviceInfos)` | 新增设备; 更新已有设备; 删除不在云端的设备 |
| 6 | `ScheduleRefreshDevices(delay)` | 定时器正确调度 |
| 7 | `OnDeviceStateChanged(did, state)` | 更新 online 状态; 通知 subDeviceState |
---
### 4.3 `miot_client_prop_test.go` — 属性刷新
| 包名 | `package miot` |
|------|-----|
| 难度 | ⭐⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `RequestRefreshProp(did, siid, piid)` | 添加到队列; 重复添加跳过 |
| 2 | `refreshPropsHandler()` | 队列为空→直接返回; http=nil→返回 |
| 3 | Timer 机制: 第一次 Request 创建 timer | 后续 Request 重用 timer |
| 4 | 并发 `RequestRefreshProp` | 无 panic, 队列完整 |
---
### 4.4 `miot_client_sub_test.go` — 订阅管理
| 包名 | `package miot` |
|------|-----|
| 难度 | ⭐⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `SubProp(did, handler, siid, piid)` | 返回 subID; subTree 有记录; 触发 RequestRefreshProp |
| 2 | `UnsubProp(did, subID)` | subTree 删除 |
| 3 | `SubEvent(did, handler, siid, eiid)` | 同上 |
| 4 | `UnsubEvent(did, subID)` | 同上 |
| 5 | `OnPropMsg(params, ctx)` | 解析 params; 匹配订阅者; 分发消息 |
| 6 | `OnPropMsg` params 含 "params" 包装层 | 正确解开 |
| 7 | `OnEventMsg(params, ctx)` | 同 OnPropMsg |
| 8 | `checkDeviceState(did)` | 三层状态检查 |
---
### 4.5 `miot_client_api_test.go` — API 调用
| 包名 | `package miot` |
|------|-----|
| 难度 | ⭐⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `SetProp(did, siid, piid, value)` | http=nil→error |
| 2 | `GetProp(did, siid, piid)` | http=nil→error |
| 3 | `Action(did, siid, aiid, inList)` | http=nil→error |
| 4 | `SetProps(params)` | http=nil→error |
| 5 | `GetProps(params)` | http=nil→error |
---
## Batch 5: 网络层 (需 Mock HTTP/MQTT/mDNS) 🟡 P4
### 5.1 `miot_cloud_test.go` — 云API客户端
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | `net/http/httptest` |
| 难度 | ⭐⭐⭐⭐ |
使用 `httptest.NewServer` 模拟 API 响应:
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `NewMIoTOauthClient(...)` | host 计算正确 (cn vs 非cn) |
| 2 | `MIoTOauthClient.GenAuthURL(...)` | URL 包含所有参数 |
| 3 | `MIoTOauthClient.State()` | 返回 hex 字符串 |
| 4 | `parseInt64(s)` | 正常解析; 含非数字字符; 空字符串 |
| 5 | `toFloat64(v)` | float64/int/int64/string 四种输入 |
| 6 | `toSlice(v)` | []interface{}→正确; nil→nil |
| 7 | `toStringSlice(v)` | []string; []interface{}→[]string |
| 8 | `isUnsupportedModel(model)` | 命中→true; 不命中→false |
| 9 | `containsString(slice, val)` | 存在→true; 不存在→false |
| 10 | `NewMIoTHttpClient(server, id, token)` | 创建成功 |
| 11 | `UpdateHTTPHeader(server, id, token)` | host/baseURL 更新 |
| 12 | `apiRequestHeaders()` | 包含 Authorization, X-Client-AppId 等 |
| 13 | `mihomeAPIGet` with mock server | 成功→解析; 401→error; 500→error |
| 14 | `mihomeAPIPost` with mock server | 同上 |
| 15 | `GetProps(params)` with mock | 正确构造请求; 解析响应 |
| 16 | `SetProp(params)` with mock | 正确构造请求 |
| 17 | `DoAction(...)` with mock | 正确构造请求; inList 处理 |
---
### 5.2 `miot_lan_test.go` — LAN 设备控制
| 包名 | `package miot_test` |
|------|-----|
| 依赖 | 无 (纯逻辑函数) |
| 难度 | ⭐⭐⭐ |
优先测纯函数,避免真实网络:
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `LanDeviceState.String()` | fresh/ping1/ping2/ping3/dead/unknown |
| 2 | `hexDecode(s)` | 正常的32位hex→16字节; 奇数长度→error; 非法字符→error |
| 3 | `md5Sum(data)` | 已知输入→已知输出 |
| 4 | `minFloat(a, b)` | a<b→a; b<a→b |
| 5 | `mustParseUint64(s)` | "123"→123 |
| 6 | `toInt(v)` | float64→int; int→int; string→0,false |
| 7 | `isNumeric(s)` | 纯数字→true; 含字母→false; 空→false |
| 8 | `trimNull(b)` | 尾部0去除 |
| 9 | `LanDevice.GenPacket(out, msg)` | 加密正确; packet 格式正确 |
| 10 | `LanDevice.DecryptPacket(data)` | 解密→原始消息; 错误MD5→error |
| 11 | `newLanDevice(...)` with valid token | 成功创建 |
| 12 | `newLanDevice(...)` with invalid hex | error |
---
### 5.3 `miot_network_test.go` — 网络监控
| 包名 | `package miot_test` |
|------|-----|
| 难度 | ⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `InterfaceStatus.String()` | add/update/remove/unknown |
| 2 | `NetworkInfo.String()` | 格式: `"name: ip/mask (netseg)"` |
| 3 | `NewMIoTNetwork(nil, nil, 0)` | 使用默认值 |
| 4 | `NewMIoTNetwork` 自定义 | ipAddrList/urlAddrList 正确存储 |
| 5 | `GetNetworkStatus()` | 初始 false |
| 6 | `SubNetworkStatus / UnsubNetworkStatus` | 注册→注销 |
| 7 | `SubNetworkInfo / UnsubNetworkInfo` | 注册→注销 |
| 8 | `UpdateAddrList(newIPs, newURLs)` | 地址列表更新; 保留已有响应时间 |
| 9 | `getNetworkInterfaces()` | 返回非 nil; 过滤 loopback |
---
### 5.4 `miot_mdns_test.go` — mDNS 服务发现
| 包名 | `package miot_test` |
|------|-----|
| 难度 | ⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `MipsServiceState.String()` | added/removed/updated/unknown |
| 2 | `MipsServiceData.validService()` | role=1+suiteMQTT→true; 其他→false |
| 3 | `MipsServiceData.ToDict()` | 所有字段正确 |
| 4 | `MipsServiceData.String()` | 非空 |
| 5 | `parseProfile(validB64)` | 正确解析 did/groupID/role/suiteMQTT |
| 6 | `parseProfile("")` | error: "empty profile" |
| 7 | `parseProfile(invalidB64)` | error |
| 8 | `parseProfile(too short)` | error: "profile too short" |
| 9 | `reverseBytes(b)` | 已知输入→已知输出 |
| 10 | `strSlicesEqual(a, b)` | 相等→true; 不相等→false; 长度不同→false |
| 11 | `NewMipsService()` | 创建成功 |
| 12 | `GetServices("")` | 初始为空 |
| 13 | `SubServiceChange / UnsubServiceChange` | 注册→注销 |
| 14 | `entryToServiceData(entry)` | 有效entry→正确解析; nil→error |
---
### 5.5 `mips_client_test.go` — MIPS 协议
| 包名 | `package miot_test` |
|------|-----|
| 难度 | ⭐⭐⭐ |
| # | 函数 | 测试内容 |
|---|------|---------|
| 1 | `MIoTDeviceState.String()` | disable/offline/online/unknown |
| 2 | `PackMipsMessage(mid, payload, from, retTopic)` | 正确打包; 空payload→error |
| 3 | `UnpackMipsMessage(data)` | 打包→解包 往返一致 |
| 4 | `MipsMessage.String()` | 非空 |
| 5 | `parseMQTTBroker(broker)` | "ssl://host:8883"→(host, 8883) |
| 6 | `NewMipsCloudClient(broker, id, user, pass)` | 创建成功 |
| 7 | `NewMipsLocalClient(did, host, port, ca, cert, key)` | 创建成功 |
| 8 | `NewMipsDeviceState(did, online, source)` | 字段正确 |
| 9 | `newPahoMQTTClient(...)` | 创建成功; Connect/Disconnect stub |
| 10 | `pahoMQTTClient` methods | Connect→connected=true; Disconnect→connected=false |
---
## 测试执行命令参考
```bash
# 运行所有测试
go test -v -count=1 ./...
# 运行特定批次
go test -v -run "TestCtrlMode|TestError|TestCommon"
# 竞态检测
go test -race ./...
# 覆盖率
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# 只测试纯函数层(无需网络)
go test -v -short ./...
```
---
## 进度追踪
| 批次 | 状态 | 测试文件 | 测试函数数(预估) | 完成日期 |
|------|------|----------|:---:|------|
| Batch 1 | ✅ 已完成 | `const_test.go` + `miot_error_test.go` + `common_test.go` | ~50 (1186行) | 2026-06-28 |
| Batch 2 | ✅ 已完成 | `miot_matcher_test.go`(增强) + `miot_storage_test.go` + `miot_i18n_test.go` | ~60 (1250行) | 2026-06-28 |
| Batch 3 | ✅ 已完成 | `miot_device_test.go` + `spec_parser_test.go` + `web_pages_test.go` | ~65 (950行) | 2026-06-28 |
| Batch 4 | ✅ 已完成 | `miot_client_test.go` + `miot_client_device_test.go` + `miot_client_prop_test.go` + `miot_client_sub_test.go` + `miot_client_api_test.go` | ~50 (920行) | 2026-06-28 |
| Batch 5 | ✅ 已完成 | `miot_cloud_test.go` + `miot_lan_test.go` + `miot_network_test.go` + `miot_mdns_test.go` + `mips_client_test.go` | ~55 (750行) | 2026-06-28 |
总计: **19 个测试文件**, ~225 个测试用例, 覆盖 19 个源文件。
---
## 注意事项
1. **旧的 `miot_test.go`**: Batch 4 完成后可删除,内容已被拆分到各独立测试文件中
2. **`-short` 标志**: 网络相关测试用 `testing.Short()` 跳过,`go test -short` 只跑纯逻辑测试
3. **临时文件**: 所有存储测试使用 `os.MkdirTemp` + `defer os.RemoveAll`
4. **并发测试**: `miot_matcher` 和 `miot_client` 相关测试必须加 `go test -race` 验证
5. **Mock HTTP**: Batch 5 的 `miot_cloud_test.go` 使用 `httptest.NewServer`,不产生真实网络请求