Golang中如何测试Gin Gonic的嵌套函数处理器

Golang中如何测试Gin Gonic的嵌套函数处理器 我有以下代码:

func CreateCluster(c *gin.Context) {

	var clusterInfo *models.MongoAtlasCreationInformation
	if err := c.ShouldBindJSON(&clusterInfo); err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest,
			gin.H{
				"error":   err.Error(),
				"message": "Invalid json sent. Please check your request json"})

		internallog.Logger.Error().Msg("Invalid json sent. Please check your request json")
	} else {
		response := mongoatlashelper.ClusterDetails(clusterInfo)

		if response.Success {
			confluence.WikiInformationUpdate(clusterInfo)
			internallog.Logger.Info().Msg("Cluster created: " + clusterInfo.MongoAtlasClusterName)
			c.JSON(http.StatusOK, response)
		} else {
			c.JSON(http.StatusBadRequest, response)
			internallog.Logger.Error().Msg("Cluster creation Failed: ")
		}
	}
}

我需要模拟以下代码:

response := mongoatlashelper.ClusterDetails(clusterInfo)

然而,无论我尝试编写什么测试来测试这段代码,它都会尝试进入该函数并执行。我尝试使用testify mock根据函数名返回结果,但似乎不起作用。

这是我目前的测试代码:

func SetUpRouter() *gin.Engine {
	router := gin.Default()
	router.Use(cors.Default())
	router.LoadHTMLGlob("templates/*")

	apiv1 := router.Group("/apiv1")

	apiv1.POST("/create", CreateCluster)

	return router
}

type MockClusterDetails struct {
	mock.Mock
}

func (m *MockClusterDetails) ClusterDetails(clusterInfo *models.MongoAtlasCreationInformation) mongoatlashelper.ClusterResult {
	args := m.Called(clusterInfo)
	return args.Get(0).(mongoatlashelper.ClusterResult)
}

type MockFindProject struct {
	mock.Mock
}

func (m *MockFindProject) FindProject(projectName string) (bool, *mongodbatlas.Project) {
	args := m.Called(projectName)
	return args.Bool(0), args.Get(1).(*mongodbatlas.Project)
}

func TestCreateCluster(t *testing.T) {
	MockClusterDetails := new(MockClusterDetails)
	MockFindProject := new(MockFindProject)

	// Create a new instance of the cluster information struct
	clusterInfo := &models.MongoAtlasCreationInformation{
		MongoAtlasProjectName:   "test-project",
		ProjectManager:          "test-manager",
		MongoDBClusterOwner:     "test-owner",
		MongoDBClusterRequester: "test-requester",
		TeamDistributionList:    "test-distribution",
		MongoAtlasClusterName:   "test-cluster",
		Environment:             "test-environment",
		DataGovernanceCategory:  "test-category",
		DataStoredDescription:   "test-description",
		AtlasProjectCIDR:        "test-cidr",
		ProviderName:            "test-provider",
		AWSRegion:               "test-region",
		AWSInstanceSizeName:     "test-instance",
		Autoscaling:             true,
		AutoscalingMin:          1,
		AutoscalingMax:          10,
		StorageScaling:          true,
		InitialStorageSize:      100,
		MongoDBVersion:          4.4,
		ExistingProject:         true,
	}

	project := &mongodbatlas.Project{
		ID:                      "test-project",
		OrgID:                   "test-org",
		Name:                    "test-project",
		ClusterCount:            1,
		Created:                 "",
		RegionUsageRestrictions: "",
	}

	// Marshal the cluster information struct to JSON
	clusterInfoJSON, _ := json.Marshal(clusterInfo)

	expectedResponse := &mongoatlashelper.ClusterResult{
		Success:  true,
		Message:  "Cluster Created: " + clusterInfo.MongoAtlasClusterName + "-" + "ProjectName: " + clusterInfo.MongoAtlasProjectName,
		Response: "200",
	}

	// Switch to test mode, so you don't get such noisy output
	gin.SetMode(gin.TestMode)

	// Set up your router, just like you did in your main function, and
	// register your routes

	router := SetUpRouter()

	MockClusterDetails.On("ClusterDetails", *clusterInfo).Return(expectedResponse)
	MockFindProject.On("findProject", "test-project").Return(true, project)

	// Create the mock request you'd like to test. Make sure the second argument
	// here is the same as one of the routes you defined in the router setup
	// block!
	req, err := http.NewRequest(http.MethodPost, "/apiv1/create", bytes.NewBuffer(clusterInfoJSON))
	if err != nil {
		t.Fatalf("Couldn't create request: %v\n", err)
	}

	// Create a response recorder so you can inspect the response
	w := httptest.NewRecorder()

	// Perform the request
	router.ServeHTTP(w, req)
	fmt.Println(w.Body)

	// Check to see if the response was what you expected
	if w.Code == http.StatusOK {
		t.Logf("Expected to get status %d is same ast %d\n", http.StatusOK, w.Code)
	} else {
		t.Fatalf("Expected to get status %d but instead got %d\n", http.StatusOK, w.Code)
	}

}

非常感谢任何帮助/建议。


更多关于Golang中如何测试Gin Gonic的嵌套函数处理器的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

感谢你的解释。我原本考虑用接口重写大部分代码,但你的方法可能是一个更快的解决方案。

我确实认为,将来我应该重写或者确保自己编写更多使用接口的代码。

更多关于Golang中如何测试Gin Gonic的嵌套函数处理器的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


使用 gin.CreateTestContext 函数为你的处理器创建测试上下文。该上下文提供了用于模拟 HTTP 请求的请求和响应对象的访问权限。

在测试上下文中为请求参数和请求体内容设置模拟值。

根据预期值验证处理器的响应码、响应头和响应体内容。

Go语言中的模拟测试与你可能习惯的方式不太一样。你不能仅在测试中替换功能——代码应该以允许这种方式编写(如果可能的话使用接口,或者使用变量,或者两者兼用)。

一个简单的解决方案是创建一个像这样的全局变量:

var(
    clusterDetailsFunc = mongoatlashelper.ClusterDetails
)

然后在你的gin处理程序中调用clusterDetailsFunc(),而不是直接调用mongoatlashelper的版本。在测试内部,你可以通过将包变量设置为一个新的测试函数来更改该函数的实现。

要测试Gin处理函数中的依赖函数,需要使用依赖注入或接口替换。以下是针对你代码的测试方案:

// 1. 首先创建接口来抽象依赖
type ClusterDetailsProvider interface {
    ClusterDetails(clusterInfo *models.MongoAtlasCreationInformation) mongoatlashelper.ClusterResult
}

// 2. 修改处理器函数以接受依赖
func CreateCluster(provider ClusterDetailsProvider) gin.HandlerFunc {
    return func(c *gin.Context) {
        var clusterInfo *models.MongoAtlasCreationInformation
        if err := c.ShouldBindJSON(&clusterInfo); err != nil {
            c.AbortWithStatusJSON(http.StatusBadRequest,
                gin.H{
                    "error":   err.Error(),
                    "message": "Invalid json sent. Please check your request json"})
            internallog.Logger.Error().Msg("Invalid json sent. Please check your request json")
        } else {
            // 现在使用注入的provider
            response := provider.ClusterDetails(clusterInfo)
            
            if response.Success {
                confluence.WikiInformationUpdate(clusterInfo)
                internallog.Logger.Info().Msg("Cluster created: " + clusterInfo.MongoAtlasClusterName)
                c.JSON(http.StatusOK, response)
            } else {
                c.JSON(http.StatusBadRequest, response)
                internallog.Logger.Error().Msg("Cluster creation Failed: ")
            }
        }
    }
}

// 3. 创建真实实现
type RealClusterDetailsProvider struct{}

func (r *RealClusterDetailsProvider) ClusterDetails(clusterInfo *models.MongoAtlasCreationInformation) mongoatlashelper.ClusterResult {
    return mongoatlashelper.ClusterDetails(clusterInfo)
}

// 4. 在路由设置中使用
func SetUpRouter(provider ClusterDetailsProvider) *gin.Engine {
    router := gin.Default()
    router.Use(cors.Default())
    router.LoadHTMLGlob("templates/*")

    apiv1 := router.Group("/apiv1")
    apiv1.POST("/create", CreateCluster(provider))

    return router
}

// 5. 测试代码
func TestCreateCluster(t *testing.T) {
    // 创建mock provider
    mockProvider := &MockClusterDetailsProvider{}
    
    clusterInfo := &models.MongoAtlasCreationInformation{
        MongoAtlasClusterName: "test-cluster",
        MongoAtlasProjectName: "test-project",
    }
    
    expectedResponse := mongoatlashelper.ClusterResult{
        Success:  true,
        Message:  "Cluster Created: test-cluster-ProjectName: test-project",
        Response: "200",
    }
    
    // 设置mock期望
    mockProvider.On("ClusterDetails", clusterInfo).Return(expectedResponse)
    
    gin.SetMode(gin.TestMode)
    
    // 使用mock provider创建路由
    router := SetUpRouter(mockProvider)
    
    clusterInfoJSON, _ := json.Marshal(clusterInfo)
    req, _ := http.NewRequest(http.MethodPost, "/apiv1/create", bytes.NewBuffer(clusterInfoJSON))
    req.Header.Set("Content-Type", "application/json")
    
    w := httptest.NewRecorder()
    router.ServeHTTP(w, req)
    
    if w.Code != http.StatusOK {
        t.Errorf("Expected status %d but got %d", http.StatusOK, w.Code)
    }
    
    // 验证mock被调用
    mockProvider.AssertCalled(t, "ClusterDetails", clusterInfo)
    
    // 验证响应
    var response mongoatlashelper.ClusterResult
    json.Unmarshal(w.Body.Bytes(), &response)
    
    if !response.Success {
        t.Error("Expected success response")
    }
}

// Mock实现
type MockClusterDetailsProvider struct {
    mock.Mock
}

func (m *MockClusterDetailsProvider) ClusterDetails(clusterInfo *models.MongoAtlasCreationInformation) mongoatlashelper.ClusterResult {
    args := m.Called(clusterInfo)
    return args.Get(0).(mongoatlashelper.ClusterResult)
}

如果你不能修改原始代码结构,可以使用猴子补丁的方式:

// 在测试文件中
var clusterDetailsFunc = mongoatlashelper.ClusterDetails

func TestCreateClusterWithMonkeyPatch(t *testing.T) {
    // 保存原始函数
    originalFunc := clusterDetailsFunc
    defer func() { clusterDetailsFunc = originalFunc }()
    
    // 替换为mock函数
    clusterDetailsFunc = func(clusterInfo *models.MongoAtlasCreationInformation) mongoatlashelper.ClusterResult {
        return mongoatlashelper.ClusterResult{
            Success:  true,
            Message:  "Mocked response",
            Response: "200",
        }
    }
    
    // 现在测试CreateCluster函数
    gin.SetMode(gin.TestMode)
    router := SetUpRouter()
    
    clusterInfo := &models.MongoAtlasCreationInformation{
        MongoAtlasClusterName: "test-cluster",
    }
    
    clusterInfoJSON, _ := json.Marshal(clusterInfo)
    req, _ := http.NewRequest(http.MethodPost, "/apiv1/create", bytes.NewBuffer(clusterInfoJSON))
    req.Header.Set("Content-Type", "application/json")
    
    w := httptest.NewRecorder()
    router.ServeHTTP(w, req)
    
    if w.Code != http.StatusOK {
        t.Errorf("Expected status %d but got %d", http.StatusOK, w.Code)
    }
}

对于你的具体测试代码问题,主要问题是mock没有实际被使用。你的测试创建了mock对象,但处理器函数仍然调用了原始的mongoatlashelper.ClusterDetails函数。你需要通过依赖注入让处理器使用你的mock对象。

回到顶部