Go single test series 2 - network test

Posted by dougp23 on Tue, 01 Feb 2022 17:41:18 +0100

This is the first in a series of tutorials on Go language unit testing from zero to slip. It introduces how to use httptest and gock tools for network testing.

In the previous "Go single test series 1 - Fundamentals of unit testing", we introduced the basic content of unit testing written in Go language.

The business scenarios in actual work are often complex. No matter whether our code is providing services as a server or we rely on the network services provided by others (calling the API interface provided by others), we usually don't want to really establish a network connection in the test process. This article specifically introduces how to test mock network in the above two scenarios.

The sample code of Go single test from zero to slide series has been uploaded to Github, click 👉🏻 https://github.com/go-quiz/golang-unit-test-demo View the complete source code.

httptest

For unit testing in the Web development scenario, if HTTP requests are involved, it is recommended that you use Go standard library {net/http/httptest} to test, which can significantly improve the testing efficiency.

In this section, we take the common gin framework as an example to demonstrate how to write unit tests for http server.

Suppose our business logic is to build an http server to provide external HTTP services. We have written a helloHandler function to handle user requests.

// gin.go
package httptest_demo

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

// Param request parameters
type Param struct {
	Name string `json:"name"`
}

// helloHandler /hello request handler
func helloHandler(c *gin.Context) {
	var p Param
	if err := c.ShouldBindJSON(&p); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"msg": "we need a name",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"msg": fmt.Sprintf("hello %s", p.Name),
	})
}

// SetupRouter routing
func SetupRouter() *gin.Engine {
	router := gin.Default()
	router.POST("/hello", helloHandler)
	return router
}

Now we need to write a unit test for the helloHandler function. In this case, we can use httptest to mock an HTTP request and response recorder, let our server receive and process the HTTP request of mock, and use the response recorder to record the response content returned by the server.

The example code of unit test is as follows:

// gin_test.go
package httptest_demo

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func Test_helloHandler(t *testing.T) {
	// Define two test cases
	tests := []struct {
		name   string
		param  string
		expect string
	}{
		{"base case", `{"name": "liwenzhou"}`, "hello liwenzhou"},
		{"bad case", "", "we need a name"},
	}

	r := SetupRouter()

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// mock an HTTP request
			req := httptest.NewRequest(
				"POST",                      // Request method
				"/hello",                    // Request URL
				strings.NewReader(tt.param), // Request parameters
			)

			// mock a response recorder
			w := httptest.NewRecorder()

			// Let the server handle the mock request and record the returned response
			r.ServeHTTP(w, req)

			// Check whether the status code meets the expectation
			assert.Equal(t, http.StatusOK, w.Code)

			// Analyze and verify whether the response content is compound with the expectation
			var resp map[string]string
			err := json.Unmarshal([]byte(w.Body.String()), &resp)
			assert.Nil(t, err)
			assert.Equal(t, tt.expect, resp["msg"])
		})
	}
}

Perform unit tests and view test results.

❯ go test -v
=== RUN   Test_helloHandler
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /hello                    --> golang-unit-test-demo/httptest_demo.helloHandler (3 handlers)
=== RUN   Test_helloHandler/base_case
[GIN] 2021/09/14 - 22:00:04 | 200 |     164.839µs |       192.0.2.1 | POST     "/hello"
=== RUN   Test_helloHandler/bad_case
[GIN] 2021/09/14 - 22:00:04 | 200 |      23.723µs |       192.0.2.1 | POST     "/hello"
--- PASS: Test_helloHandler (0.00s)
    --- PASS: Test_helloHandler/base_case (0.00s)
    --- PASS: Test_helloHandler/bad_case (0.00s)
PASS
ok      golang-unit-test-demo/httptest_demo     0.055s

Through this example, we have mastered how to use httptest to write unit tests for request processing functions in HTTP Server services.

gock

The above example describes how to write unit tests for request processing functions in the HTTP Server service class scenario. If we are requesting external APIs in the code (for example, calling other services to obtain return values through APIs), how to write unit tests?

For example, we have the following business logic codes that rely on external APIs: http://your-api.com/post Data provided.

// api.go

// ReqParam API request parameters
type ReqParam struct {
	X int `json:"x"`
}

// Result API returns the result
type Result struct {
	Value int `json:"value"`
}

func GetResultByAPI(x, y int) int {
	p := &ReqParam{X: x}
	b, _ := json.Marshal(p)

	// Call API s of other services
	resp, err := http.Post(
		"http://your-api.com/post",
		"application/json",
		bytes.NewBuffer(b),
	)
	if err != nil {
		return -1
	}
	body, _ := ioutil.ReadAll(resp.Body)
	var ret Result
	if err := json.Unmarshal(body, &ret); err != nil {
		return -1
	}
	// Here are some logical processing for the data returned by the API
	return ret.Value + y
}

When writing unit tests for business code like the above, if we don't want to really send requests during the test process or the dependent external interface has not been developed, we can mock the dependent API in the unit test.

Recommended here gock This library.

install

go get -u gopkg.in/h2non/gock.v1

Use example

Use gock to mock the external API, that is, mock specifies the parameters to return the agreed response content. In the following code, mock two sets of data to form two test cases.

// api_test.go
package gock_demo

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/h2non/gock.v1"
)

func TestGetResultByAPI(t *testing.T) {
	defer gock.Off() // Refresh pending mock after test execution

	// When mock requests an external api, it passes parameter x=1 and returns 100
	gock.New("http://your-api.com").
		Post("/post").
		MatchType("json").
		JSON(map[string]int{"x": 1}).
		Reply(200).
		JSON(map[string]int{"value": 100})

	// Call our business function
	res := GetResultByAPI(1, 1)
	// Verify whether the returned results meet the expectations
	assert.Equal(t, res, 101)

	// When mock requests an external api, it passes parameter x=2 and returns 200
	gock.New("http://your-api.com").
		Post("/post").
		MatchType("json").
		JSON(map[string]int{"x": 2}).
		Reply(200).
		JSON(map[string]int{"value": 200})

	// Call our business function
	res = GetResultByAPI(2, 2)
	// Verify whether the returned results meet the expectations
	assert.Equal(t, res, 202)

	assert.True(t, gock.IsDone()) // Assertion mock triggered
}

Execute the unit test written above and look at the test results.

❯ go test -v
=== RUN   TestGetResultByAPI
--- PASS: TestGetResultByAPI (0.00s)
PASS
ok      golang-unit-test-demo/gock_demo 0.054s

The test results are completely consistent with the expectations.

In this example, in order to let you clearly understand the use of gock, I deliberately did not use table driven testing. Leave a little homework for you: rewrite this unit test into a form driven test style by yourself, as a review and test of the last two tutorials.

summary

How to deal with external dependencies is the most common problem when writing unit tests for code in daily work development. This paper introduces how to use httptest and gock tool mock related dependencies. In the next article, we will go further and introduce in detail how to write unit tests for scenarios that rely on MySQL and Redis.

Topics: Go