How to test Gin Gonic Handler (function within a function)

HI,

I have the following code:

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: ")
		}
	}
}

I need to mock out

response := mongoatlashelper.ClusterDetails(clusterInfo)

However no matter what I try to write to test this code, it tries to go into that function and execute. I have tried using testify mock to return a result based on the function name but it doesn’t seem to work.

This is what the test looks like currently

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)
	}

}

I would appreciate any help / advice.

Mocks in Go don’t work quite the same as you might be used to. You can’t swap out functionality only within the test - the code should be written in a way that allows it (either with an interface if possible or with a variable or both).

A simple solution is to create a global variable like this:

var(
    clusterDetailsFunc = mongoatlashelper.ClusterDetails
)

Then call clusterDetailsFunc() within your gin handler instead of the mongoatlashelper version directly. From within your test, you can change the implementation of that function by setting the package variable equal to a new test function.

1 Like

@thenorthnate thank you for explaining. I was thinking of rewriting most of the code with interfaces, but I think your way may be a quicker solution.

I do think I should in the future rewrite or make sure I write code with more interfaces

1 Like

Use the gin.CreateTestContext function to create a test context for your handler. This context provides access to request and response objects for simulating HTTP requests.

Set mock values for request parameters and body content within the test context.

Verify the handler’s response code, headers, and body content against expected values.