At this post, I have brief introduced how to test our gin APIs.
And one test case tests one API, maybe this API including thousands functions, this kind of test we allways call Integration Test.

The Integration Test always care whether the API works well or not, it does’t care the codes coverage and whether a certain function works as expect or not.

So, we need to write Unit Test to ensure our program robust and keeps the issues number at a very low level.

How to write a unit test for go?

It’s very easy for us to write a basic test case in go.
We can simply create a file which only subprefix is “_test.go.”, then command go test -v . to execute it.
Here is an example:
add.go

1
2
3
func Add(a, b int) (c int) {
    return a + b
}

And our test file:
add_test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "testing"

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

func TestAdd(t *testing.T) {
    var assert = assert.New(t)
    type S = struct {
        A int
        B int
        C int
    }
    var data = []S{
        {1, 2, 3},
        {-1, -1, -2},
        {0, -1, -1},
        {999999999999, 1, 1000000000000},
    }
    for _, v := range data {
        assert.Equal(v.C, Add(v.A, v.B))
    }
}

When I ran go test -run ^TestAdd$ test, and I got:

=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      test    0.307s

How to mock function when a function includes multiple calls?

using gomonkey to mock common function

OK, basic lesson is over, let’s do some a little more deep things.
Consider a very common situation, we have a function named A. And in function A, it need to call function B, call function C, how should we test A? Like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

func B(a, b string) (result string, err error) {
    // some logic
}

func C(a, b string) (result string, err error) {
    // some logic 
}

func A(a, b string) (score int, err error) {
    // process the parameters a and b -> a1, b1
    result1, err = B(a1, b1)
    if err != nil {
        return 0, err
    }
    // process result -> a2, b2
    result2, err = C(a2, b2)
    if err != nil {
        return 0, err
    }
    // process the result1 and result2 to got the score

    return score, nil    
}

In the example above, I have a function A, in A, it will do some very complex logic to process the parameters a and b to get a score.
I don’t want to care the function B and C due to I have writern unit cases for them and ensure them are run as my expect.
We can use a libaray gomonkey to mock the function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
    "testing"

    "github.com/agiledragon/gomonkey/v2"

    "github.com/stretchr/testify/assert"
)
func TestA(t *testing.T) {
    assert := assert.New(t)
    var err error
    mockFunc1 := gomonkey.ApplyFunc(B, func(a, b string) (string, error) {
        return "", nil
    })
    mockFunc2 := gomonkey.ApplyFunc(C, func(a, b string) (string, error) {
        return "", nil
    })
    defer mockFunc1.Reset()
    defer mockFunc2.Reset()

    // mocke data then check
    output, err := A(a, b)
    assert.NoError(err)
    assert.Equal(expectData, output)
}

We can mock function B and C, and output the data what we want.
And the calls in A will not call the real function B and C.

using gomonkey to mock function of a struct

We can use gomonkey to mock the function of a struct, including public and private

  • public function

Here is the example:

appserver.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

type AppServer struct {}

func(a *AppServer) A(a, b string) (result string, err error) {
    // do some logic -> a1, b1
    tempResult, err = a.b(a1, b1)
    if err != nil {
        return 
    }
    // process the tempResult 
    // logic
    return result, nil
}
func (a *AppServer) b(a, b string) (result string, err error) {
    // do some logic
}

var appServer = AppServer{}

AppServer has 2 functions: A and b, A is a public function, we can call this function from outer of AppServer, but b is a private function, only call it inner AppServer.

apiserver.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

func Process(a, b string) (score int, err error) {
    // process a and b -> a1, b1
    result, err = appServer.A(a1, b1)
    if err != nil {
        return 
    }
    // process result -> score
    return score, nil
}

Function Process calls public function A of the struct AppServer, how we test Process by mocking function A:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
    "reflect"
    "testing"

    "github.com/agiledragon/gomonkey/v2"

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

func TestProcess(t *testing.T) {
    var assert = assert.New(t)

    mockFunc := gomonkey.ApplyMethod(reflect.TypeOf(&appServer), "A",
        func(_ *AppServer, a, b string) (result string, err error) {
            // mock result
            return result, nil
        })
    defer mockFunc.Reset()

    // mock a, b
    output, err = Process(a, b)
    assert.NoError(err)
    assert.Equal(expect, output)
}
  • private function

If we want to test the function A of AppServer, we can mock the private function b:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import (
    "reflect"
    "testing"

    "github.com/agiledragon/gomonkey/v2"

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

var testAppServer = AppServer{}

func TestProcess(t *testing.T) {
    var assert = assert.New(t)

    mockFunc := gomonkey.ApplyPrivateMethod(reflect.TypeOf(&testAppServer), "b",
        func(_ *AppServer, a, b string) (result string, err error) {
            // mock result
            return result, nil
        })
    defer mockFunc.Reset()

    // mock a, b
    output, err = testAppServer.A(a, b)
    assert.NoError(err)
    assert.Equal(expect, output)
}

How to simulate request parameters?

And I will show the full unit test cases of a webserver bases on gin:

apiserver.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

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

type ApiServer struct{}

func (a *ApiServer) Query(c *gin.Context) {
    var err error
    userName := c.Param("userName")
    var userInfo = UserInfo{}
    if userInfo, err = appServer.Query(userName); err != nil {
        c.JSON(404, []byte(`{"error":"User not found"}`))
        return
    }
    c.JSON(200, userInfo)
    return
}

func (a *ApiServer) Create(c *gin.Context) {
    var err error
    var userInfo UserInfo
    if err = c.ShouldBindJSON(&userInfo); err != nil {
        c.JSON(400, []byte(`{"error":"Invalid request body"}`))
        return
    }
    if err = appServer.Create(userInfo); err != nil {
        c.JSON(500, []byte(`{"error":"Failed to create user"}`))
        return
    }
    c.JSON(204, nil)
    return
}

func (a *ApiServer) QueryUserList(c *gin.Context) {
    var err error
    var req = struct {
        PageSize int `form:"pageSize" binding:"required"`
        Page     int `form:"page" binding:"required" `
    }{}
    if err = c.ShouldBind(&req); err != nil {
        c.JSON(400, []byte(`{"error":"Invalid url value"}`))
        return
    }
    var usersInfo []UserInfo
    if usersInfo, err = appServer.QueryList(req.Page, req.PageSize); err != nil {
        c.JSON(500, []byte(`{"error":"Failed to query users"}`))
        return
    }
    c.JSON(200, usersInfo)
    return
}

appserver.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

type UserInfo struct {
    ID    string `json:"id"`
    Name  string `json:"name" binding:"required"`
    Age   int    `json:"age" binding:"required"`
    Email string `json:"email" binding:"required"`
}

type AppServer struct{}

func (a *AppServer) Query(userName string) (userInfo UserInfo, err error) {
    // do a database query operation
    return
}

func (a *AppServer) Create(userInfo UserInfo) (err error) {
    // do a database create operation
    return
}

func (a *AppServer) QueryList(page, size int) (usersInfo []UserInfo, err error) {
    // do a database query operation
    return
}

var appServer = AppServer{}

We assume all the function of AppServer have been tested passed, which means we only need to write the unit test cases for ApiServer.

We need to create a universal request function to simulate a real request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "io"
    "net/url"
    "net/http"
    "net/http/httptest"
)

func request(function func(c *gin.Context), reqBody []byte, urlParams []gin.Param, urlValues url.Values) (resBody []byte, statusCode int) {
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext()
    c.Request = &http.Request{
        Header: make(http.Header),
        URL:    &url.URL{},
    }
    // set reqBody
    c.Request.Body = io.NopCloser(bytes.Newbuffer(reqBody))
    // set url value
    c.Request.URL.RawQuery = urlValues.Encode()
    // set urlParams
    for _, param := range urlParams {
        c.Params = append(c.Params, param)
    }
    function(c)
    body, _ := io.ReadAll(w.Body)
    return body, w.Code
}

We noticed that Gin.Context used c.Params = append(c.Params, param) to create the url parameters.

  • url parameters To simulate the url parameters, we can use urlParams := []gin.Param{{Key: "userName", Value: ""}}.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func TestQuery(t *testing.T) {
    var assert = assert.New(t)
    mockFuncQuery := gomonkey.ApplyMethod(reflect.TypeOf(&appServer), "Query",
        func(_ *AppServer, userName string) (userInfo UserInfo, err error) {
            if userName == "" {
                return UserInfo{}, errors.New("not Found")
            }
            return userInfo{Name: userName}, nil
        })
    defer mockFuncQuery.Reset()

    var urlParams []gin.Param

    var body []byte
    var statusCode int

    // 404
    urlParams = []gin.Param{{Key: "userName", Value: ""}}
    body, statusCode = request(testApiServer.Query, nil, urlParams, nil)
    assert.Equal(404, statusCode)
    assert.Equal([]byte(`"error":"User not found"`), body)

    // 200
    urlParams = []gin.Param{{Key: "userName", Value: "Jack"}}
    body, statusCode = request(testApiServer.Query, nil, urlParams, nil)
    assert.Equal(404, statusCode)
    output := UserInfo{}
    err := json.Unmarshal(body, &output)
    assert.NoError(err)
    assert.Equal(UserInfo{Name: "Jack"}, output)
}
  • request body

The request body is always json string, so we only create a bytes data to simulate it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func TestCreate(t *testing.T) {
    var assert = assert.New(t)
    var internalError bool
    mockFuncQuery := gomonkey.ApplyMethod(reflect.TypeOf(&appServer), "Create",
        func(_ *AppServer, userInfo UserInfo) (err error) {
            if internalError {
                return errors.New("internalError")
            }
            return nil
        })
    defer mockFuncQuery.Reset()

    var reqBody []byte

    var body []byte
    var statusCode int

    // 400 invalid req body, Name, age and email are must keys.
    // age type error 
    reqBody = []byte(`{"name":"Jack","Age":"12","Email":"jack@google.com"}`)
    body, statusCode = request(testApiServer.Create, reqBody, nil, nil)
    assert.Equal(400, statusCode)
    assert.Equal([]byte(`"error":"Invalid request body"`), body)
    // no email field
    reqBody = []byte(`{"name":"Jack","Age":12}`)
    body, statusCode = request(testApiServer.Create, reqBody, nil, nil)
    assert.Equal(400, statusCode)
    assert.Equal([]byte(`"error":"Invalid request body"`), body)
    // name is empty
    reqBody = []byte(`{"name":"","Age":12,"Email":"jack@google.com"}`)
    body, statusCode = request(testApiServer.Create, reqBody, nil, nil)
    assert.Equal(400, statusCode)
    assert.Equal([]byte(`"error":"Invalid request body"`), body)

    // internalError
    internalError = true
    reqBody = []byte(`{"name":"Jack","Age":12,"Email":"jack@google.com"}`)
    body, statusCode = request(testApiServer.Create, reqBody, nil, nil)
    assert.Equal(500, statusCode)
    assert.Equal([]byte(`"error":"Failed to create user"`), body)

    // success -> 204
    internalError = false
    reqBody = []byte(`{"name":"Jack","Age":12,"Email":"jack@google.com"}`)
    _, statusCode = request(testApiServer.Create, reqBody, nil, nil)
    assert.Equal(204, statusCode)
}
  • url value Let’s simulate the url value, the format of url value should be like http://ip:port?page=1&pageSize=10, and the page and pageSize are url values. Here I can use url.Values, and the it provides function Add to add our url valus.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func TestQueryUserList(t *testing.T) {
    var assert = assert.New(t)
    mockFuncQuery := gomonkey.ApplyMethod(reflect.TypeOf(&appServer), "QueryList",
        func(_ *AppServer, page, size int) (usersInfo []UserInfo, err error) {
            if page < 0 || size < 0 {
                return nil, errors.New("internalError")
            }
            return []UserInfo{{Name: "Jack"}}, nil
        })
    defer mockFuncQuery.Reset()
    var body []byte
    var statusCode int
    var urlValues = url.Values{}
    // no page and no pageSize
    body, statusCode = request(testApiServer.QueryUserList, nil, nil, urlValues)
    assert.Equal(400, statusCode)
    assert.Equal([]byte(`{"error":"Invalid url value"}`), body)

    // page < 0 || pageSize < 0 -> internal error
    urlValues.Add("page", "-1")
    urlValues.Add("pageSize", "-1")
    body, statusCode = request(testApiServer.QueryUserList, nil, nil, urlValues)
    assert.Equal(500, statusCode)
    assert.Equal([]byte(`{"error":"Failed to query users"}`), body)

    // normal
    urlValues.Add("page", "1")
    urlValues.Add("pageSize", "10")
    body, statusCode = request(testApiServer.QueryUserList, nil, nil, urlValues)
    assert.Equal(200, statusCode)
    var output []UserInfo
    err := json.Unmarshal(body, &output)
    assert.NoError(err)
    assert.ElementsMatch([]UserInfo{{Name: "Jack"}}, output)
}