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.