You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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的实现,但这种方式属于“黑魔法”,可能有副作用(比如影响其他测试、编译优化时失效)。

测试实现

  1. 安装依赖:go get github.com/bouk/monkey
  2. 编写测试代码:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 03:15:00