第 15 章:让测试覆盖真实控制器语义
选择测试层的依据
字符串编码、期望对象构造和条件合并适合快速单元测试。资源版本冲突、默认值、status 子资源、垃圾回收和 CRD 版本行为必须交给真实 API server。fake client 对很多服务器行为只做近似,不能因为内存对象更新成功就推断真实集群也会接受。
本项目直接用固定 kind 节点镜像执行集成套件。它付出启动成本,换取真实准入、默认、etcd 存储和内建控制器参与。测试环境仍是单节点容器化集群,不覆盖生产网络拓扑、控制面高可用和真实云依赖。
所有场景位于一个父测试之下,按名字独立记录。每个场景创建自己的命名空间并注册清理,避免不同实例名称相同导致资源串扰。CRD 版本切换安排在末尾,因为它改变整个测试集群的全局 API,不能与前面假定 alpha 存储版本的场景随意并行。
断言最终状态,也断言没有误操作
首次收敛测试检查副本数、Service selector 和 observedGeneration。拒绝无关 Service 的测试则检查错误返回,同时确认原端口和 owner reference 未被修改。后者是一条负向事实:错误不应伴随越权接管。
冲突测试要求真实 409,并检查并发注解保留。状态竞态测试要求不能提前确认新 generation。finalizer 测试既检查最终删除,也检查阻塞期间对象与登记仍存在。每条测试都从一个可能的错误实现出发,确保断言能够把它识别出来,而非只重复函数返回值。
HTTP 测试不只检查 Pod Ready。它从独立客户端 Pod 请求 Service DNS,记录修改前后正文及镜像身份。这样端口、配置生成和资源关系至少有一条端到端证据。三次请求是配置收敛检查,不是性能测试或统计可靠性基准。
等待条件,不依赖固定休眠
真实集群是异步系统。创建 Deployment 后立刻检查 Pod,通常会看到尚未完成的中间状态。固定睡眠既浪费时间,也容易在慢 runner 上失败。本项目通过有截止时间的 eventually 轮询具体条件,在条件满足时立即继续,超时则保留失败证据。
等待条件本身也要准确。只看 availableReplicas 为一,可能仍是旧版本 Pod;因此 HTTP 变更测试同时检查 Deployment 已观察其 generation,并且更新副本数达到目标。对垃圾回收则等待 API 返回 NotFound,不能把删除请求接受当作完成。
测试不能把超时改成无限循环来提高通过率。失败需要明确终点,才能让 CI 上传现场并释放集群。若环境变化导致稳定超时,应先查原始事件和日志,确认是资源预算、镜像拉取还是控制逻辑,再决定是否调整截止时间。
零跳过要核对具体名字
Go 的无测试命令包会产生包级 skip 事件,它不等于集成场景被跳过。证据收集器拒绝带 Test 名称的 skip 和所有 fail,再从源码提取命名子场景,与实际 pass 集合逐一比较。仅判断退出码为零,可能漏掉测试因为环境缺失主动 Skip 的情况。
KUBECONFIG 或独立二进制缺失时,套件直接失败,不把未执行的真实实验包装为通过。inventory 的固定数量还要求添加或删除场景时显式更新证据规则,促使维护者重新评审验证范围。
从下载包再次执行
发布流水线构建实验 ZIP 后,将它解压到独立目录,再用其中的脚本创建另一套真实 kind 集群并执行同样场景。这能发现源码目录隐含依赖、遗漏 YAML 或 go.sum,以及路径只在仓库布局下成立的问题。
第二次执行成本较高,但它直接验证读者下载得到的入口,而不是仅相信打包清单。练习是故意从归档清单移除 beta CRD,再运行独立包验证,确认版本场景因缺文件失败;随后恢复清单。测试覆盖发布路径,才能把“可下载”提升为“可独立复现”。
通过之后仍要阅读实现
测试能够证明已经提出的断言,却无法自动发现所有遗漏要求。审稿时应反向检查每项书中承诺是否有对应场景,以及每个场景是否真正经过所描述路径。例如声明真实重启,就必须有独立进程终止;声明服务转发,就应确认请求没有绕开Service。
还要检查清理代码只删除自己创建的环境,尤其是失败路径。可复现实验不应以污染读者集群为代价,测试隔离本身也属于交付质量。