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

21 KiB
Raw Permalink Blame History

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

测试执行命令参考

# 运行所有测试
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,不产生真实网络请求