如何用Ginkgo编写单元测试Mock Clientset测试K8s节点获取接口
问题描述
我正在学习用Ginkgo测试框架编写API与普通函数的单元测试,目前卡在Mock相关操作上。我有一个用于获取Kubernetes集群节点列表的GET接口,想要编写单元测试验证其返回响应,但不知如何Mock setupKube函数,使其返回包含指定节点(例如3个节点)的fakeclientset。此前测试kube.GetAllNodes函数时,直接传入fakeclientset即可,但现在需要Mock setupKube让它返回假客户端集,以便在单元测试中得到预期结果。我了解过猴子补丁、高阶函数等方法,但仍不清楚如何实现Mock返回。当前运行单元测试时,由于不在集群环境中导致报错,卡在Mock环节。相关代码如下:
func GetAllNodes(c *gin.Context) { setHeaders(c) clientset, err := setupKube() if err != nil { errorResponse := buildErrors(http.StatusInternalServerError, err) c.JSON(errorResponse.HTTPStatusCode, errorResponse) return } nodes, err := kube.GetAllNodes(clientset) if err != nil { errorResponse := buildErrors(http.StatusInternalServerError, err) c.JSON(errorResponse.HTTPStatusCode, errorResponse) return } if len(nodes) == 0 { c.JSON(http.StatusNoContent, nodes) return } c.JSON(http.StatusOK, nodes) } // Private Functions func setupKube() (kubernetes.Interface, error) { // checks for pod running in cluster config, err := rest.InClusterConfig() if err != nil { return nil, fmt.Errorf("error creating in-cluster config: %v", err) } // creates the clientset clientset, err := kubernetes.NewForConfig(config) if err != nil { return nil, fmt.Errorf("error creating clientset: %v", err) } return clientset, nil }
解决方案
方案一:依赖注入重构(推荐)
直接Mock私有函数setupKube比较麻烦,最稳妥的方式是重构代码,将客户端获取逻辑抽象为可替换的依赖,方便测试时注入Mock实现。
重构主代码
把setupKube定义为包级变量,默认指向原有实现:
import ( "fmt" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) // 定义客户端获取函数类型 type KubeClientGetter func() (kubernetes.Interface, error) // 包级变量,默认使用原有集群内配置逻辑 var setupKube KubeClientGetter = func() (kubernetes.Interface, error) { config, err := rest.InClusterConfig() if err != nil { return nil, fmt.Errorf("error creating in-cluster config: %v", err) } clientset, err := kubernetes.NewForConfig(config) if err != nil { return nil, fmt.Errorf("error creating clientset: %v", err) } return clientset, nil } func GetAllNodes(c *gin.Context) { setHeaders(c) clientset, err := setupKube() // 调用包级变量的函数 if err != nil { errorResponse := buildErrors(http.StatusInternalServerError, err) c.JSON(errorResponse.HTTPStatusCode, errorResponse) return } nodes, err := kube.GetAllNodes(clientset) if err != nil { errorResponse := buildErrors(http.StatusInternalServerError, err) c.JSON(errorResponse.HTTPStatusCode, errorResponse) return } if len(nodes) == 0 { c.JSON(http.StatusNoContent, nodes) return } c.JSON(http.StatusOK, nodes) }
Ginkgo测试实现
测试前替换setupKube为Mock函数,返回预定义节点的fake clientset:
import ( "encoding/json" "net/http" "net/http/httptest" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/fake" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // 保存原始实现,测试后恢复 var originalSetupKube KubeClientGetter = setupKube var _ = ginkgo.Describe("GetAllNodes API", func() { ginkgo.BeforeEach(func() { // 注入Mock实现,返回包含3个节点的fake clientset setupKube = func() (kubernetes.Interface, error) { fakeClientset := fake.NewSimpleClientset( &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-1"}}, &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-2"}}, &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-3"}}, ) return fakeClientset, nil } }) ginkgo.AfterEach(func() { // 恢复原始实现,避免影响其他测试 setupKube = originalSetupKube }) ginkgo.It("返回3个节点并响应200 OK", func() { // 创建Gin测试上下文 w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest(http.MethodGet, "/nodes", nil) // 调用API函数 GetAllNodes(c) // 验证响应 gomega.Expect(w.Code).To(gomega.Equal(http.StatusOK)) var nodes []corev1.Node gomega.Expect(json.Unmarshal(w.Body.Bytes(), &nodes)).To(gomega.Succeed()) gomega.Expect(len(nodes)).To(gomega.Equal(3)) }) ginkgo.It("无节点时返回204 No Content", func() { // 替换为返回空节点列表的Mock setupKube = func() (kubernetes.Interface, error) { return fake.NewSimpleClientset(), nil } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest(http.MethodGet, "/nodes", nil) GetAllNodes(c) gomega.Expect(w.Code).To(gomega.Equal(http.StatusNoContent)) }) })
方案二:猴子补丁(不推荐)
如果不想重构代码,可以用猴子补丁工具替换私有函数setupKube的实现,但这种方式属于“黑魔法”,可能有副作用(比如影响其他测试、编译优化时失效)。
测试实现
- 安装依赖:
go get github.com/bouk/monkey - 编写测试代码:
import ( "encoding/json" "net/http" "net/http/httptest" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" "github.com/bouk/monkey" "k8s.io/client-go/kubernetes/fake" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = ginkgo.Describe("GetAllNodes API", func() { var patch *monkey.Patch ginkgo.BeforeEach(func() { // 猴子补丁替换私有函数setupKube patch = monkey.Patch(setupKube, func() (kubernetes.Interface, error) { fakeClientset := fake.NewSimpleClientset( &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-1"}}, &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-2"}}, &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-3"}}, ) return fakeClientset, nil }) }) ginkgo.AfterEach(func() { // 撤销补丁 patch.Unpatch() }) ginkgo.It("成功返回3个节点", func() { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest(http.MethodGet, "/nodes", nil) GetAllNodes(c) gomega.Expect(w.Code).To(gomega.Equal(http.StatusOK)) var nodes []corev1.Node gomega.Expect(json.Unmarshal(w.Body.Bytes(), &nodes)).To(gomega.Succeed()) gomega.Expect(len(nodes)).To(gomega.Equal(3)) }) })
注意事项
猴子补丁不适合长期维护的代码,一旦代码结构变化(比如setupKube函数签名修改),补丁会失效,且难以排查问题。优先选择依赖注入的重构方案。
内容的提问来源于stack exchange,提问作者Tom
相关产品推荐
相关产品推荐

