envtest 是轻量级本地 kubernetes api server 模拟环境,仅提供 fake api server 和内存 etcd,不运行 kubelet、不调度 pod、不执行控制器逻辑;因此不能仅靠它测试控制器行为,必须显式启动控制器并连接该环境,否则 reconcile 不会触发,导致测试失效。

EnvTest 是什么,为什么不能只用它测控制器逻辑
EnvTest 不是“开箱即用的测试集群”,它只提供一个轻量级、本地启动的 Kubernetes API Server 实例和 CRD 注册能力。它不运行 kubelet、不调度 Pod、不执行控制器——你看到的 StatefulSet 或 Pod 对象只是被 API Server 接收并存进 etcd 模拟存储里,不会真实创建容器。所以,光靠 EnvTest 启动 client 就去断言资源是否“被创建”,结果永远是假阳性。
常见错误现象:client.Create(ctx, &pod) 成功,但后续 client.Get 却拿不到;或 Eventually 等半天也不触发,因为没人监听这个 Pod 的事件。
- EnvTest 本质是 fake API server + 内存 etcd,适合验证 CRD Schema、对象序列化、RBAC 权限、以及 controller-runtime 的 client 行为
- 要测控制器本身是否按预期响应事件(比如创建
MySQLCluster后是否生成StatefulSet),必须显式启动你的控制器,并让它连接到这个 EnvTest 集群 - 官方
suite_test.go模板默认只建 client,不启 controller,这是绝大多数人卡住的第一步
怎么在 BeforeSuite 中正确启动控制器
你得手动把控制器注册进 ctrl.Manager,再用 go k8sManager.Start(...) 启起来。这一步漏掉,所有 Reconcile 逻辑都不会执行。
实操建议:
- 删掉模板里直接用
k8sClient做操作的写法,改用从 manager 获取的 client:k8sManager.GetClient() - 确保控制器 setup 成功:
reconciler.SetupWithManager(k8sManager)必须返回nil,否则后续 panic - 用
ctrl.SetupSignalHandler()启动 manager,别用signal.Notify自己搞信号处理,否则测试进程可能不退出 - 加
time.Sleep(100 * time.Millisecond)在 controller 启动后(可选),避免 race:manager 刚 start,Reconcile 就被 event 触发,但 informer cache 还没 warm up
关键代码片段(补在 BeforeSuite 里):
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
Expect(err).ToNot(HaveOccurred())
<p>err = (&MySQLClusterReconciler{
Client: k8sManager.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("MySQLCluster"),
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())</p><p>go func() {
err = k8sManager.Start(ctrl.SetupSignalHandler())
Expect(err).ToNot(HaveOccurred())
}()
</p>
测试中如何避免并发写 status 导致条件丢失
多个测试用例并发跑时,如果都调 client.Status().Update() 全量覆盖 CR 的 status 字段,就会互相覆盖对方刚写的 conditions,导致某个测试里设置的 Ready=False 被另一个测试的 Ready=True 覆盖掉。
正确做法是只 patch 变更字段,且每次更新前先 Get 当前 status:
- 不要直接
client.Status().Update(ctx, cr),而应先client.Get(ctx, key, cr),再修改cr.Status.Conditions,最后client.Status().Patch(...) - 用
controllerutil.SetStatusCondition(来自sigs.k8s.io/controller-runtime/pkg/controller/controllerutil)来安全追加/更新 condition,它会自动处理去重和时间戳 - 测试里检查 condition 时,别只看
len(cr.Status.Conditions) > 0,要明确断言type和status,例如:Expect(condition.Type).To(Equal(mysqlv1alpha1.ConditionReady))
EnvTest 测试失败时最该先查哪三件事
90% 的 EnvTest 集成测试失败,问题不在控制器逻辑,而在环境准备链上。别急着 debug Reconcile,先确认:
-
CRDDirectoryPaths是否指向正确的 CRD YAML 路径?路径错或文件名大小写不对(如mysqlclusters.yamlvsMySQLClusters.yaml),会导致AddToScheme失败,后续所有Get都报no kind "MySQLCluster" is registered -
scheme是否注册了全部依赖类型?除了你的 CRD(mysqlv1alpha1.AddToScheme),还得有appsv1.AddToScheme、corev1.AddToScheme,缺一个,list/watch 就会 panic - 测试函数是否用了
Context/It嵌套过深?Ginkgo 的Eventually默认 timeout 是 1s,而 controller 第一次 reconcile 可能因 informer cache 初始化延迟 2–3s,直接导致超时失败;显式传timeout: 30 * time.Second更稳妥
EnvTest 的“集成”是有限度的:它集成的是 API 层,不是整个 Kubernetes 控制平面。想验证 Pod 是否真正 Ready,得换 e2e 测试;想确认 StatefulSet 是否挂载了 PVC,得看 controller 是否正确设置了 volumeClaimTemplates 并触发了 PVC 创建逻辑——这些都发生在 controller 代码里,而不是 EnvTest 环境本身。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










