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