As the pre-post said, Gin is a famouse web framework writen by Go. I love it and always use it.
When I wrote a web server using Go-Gin, how do I test the APIs? This may confused me, only use a command-line tool to verify them? It’s seems ridiculous and non-product.
We need to have a full-check for the APIs and it’s better to make it automatic, then we need to integrate it in the CI-CD pipline, make sure that it will run automaticly after we commit codes.

write a Go-Gin service

It’s very easy to write a web server using Go-Gin, here’s an example of exporting the information of an user.
The very first step, we need to create a workplace named webserver.
The architechture is:

1
2
3
4
5
6
webserver
    -- module
        -- user.go
    -- main.go
    -- go.sum
    -- go.mod

The logic codes are under a module file:
module/user.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
package module

import (
    "net/http"

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

type UserInfo struct {
    Nickname string `json: "nickname"`
    UserID   string `json:"userID"` // unique for all users
    Gender   int    `json:"gender"` // 0 - male 1 - female
    BirthDay string `json:"birthday"`
}

func GetUser(c *gin.Context) {
    name := c.Request.FormValue("name")
    if name == "Joe" {
        var user = UserInfo{
            Nickname: "Joe",
            UserID:   "113113",
            Gender:   0,
            BirthDay: "1990-01-01",
        }
        c.IndentedJSON(http.StatusOK, user)
        return
    }

    c.IndentedJSON(http.StatusBadRequest, nil)
    return
}

main.go is our start process, here we register all the routers.
main.go

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

import (
    "webserver/module"

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

func main() {
    r := gin.Default()
    r.GET("/user", module.GetUser)
    r.Run()
}

We set a form value of name here, if only the name equals Joe, the info of Joe will be sent out, but other name will get the HTTP status 400.
Then run go run main.go to start the webserver.

test the APIs using postman

Postman is a well-known GUI API test tool, easy and elegant to use, ofcourse, here you can use other GUI tools you like, suck as RapidApi and Apifox.

When I set the name Joe, I’ll get the info of Joe:
pSiMVWq.png But others will only get BadRequest:
pSiMlTJ.png

You can use your own tool to test it.

write an unit-test for the APIs

We’ve already test out API by using postman, it seems OK now. But it’s only a very good tool for programmer, although it clear and easy to use, help us fix bug and check the API realtime.
Somtimes, we change a universal function, it’s may effect to all the apis, although we sure the function is 100 percent OK and have beed tested full, but we still need to query all the APIs manually in the GUI software Postman. When you have 1000 APIs, you may crazy.
Actually, Postman provides us a test tool and mock data now, you can test all your APIs by one click, but if a lazy boy changed the universal function, just testing it but forget to run the test? This may case a bad issue.
We still need to write test code for it, we need to write the test code and integrate in Git pipeline, only the pipeline passed, your code will be commit successfully.
Now, Gin provides a good function to call APIs directly.
Create a new file: test/api_test.go
Now, your architechture may like this:

1
2
3
4
5
6
7
8
webserver
    -- module
        -- user.go
    -- test
        -- api_test.go
    main.go
    go.sum
    go.mod

module/user.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
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
    "webserver/module"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func TestGetUser(t *testing.T) {
    var url = "/user"
    r := gin.Default()
    r.GET(url, module.GetUser)

    w := httptest.NewRecorder()
    req, _ := http.NewRequest(http.MethodGet, "/user", nil)
    r.ServeHTTP(w, req)
    assert.Equal(t, http.StatusBadRequest, w.Code)

    var name = "Joe"
    req2, _ := http.NewRequest(http.MethodGet, "/user?name="+name, nil)
    w = httptest.NewRecorder()
    r.ServeHTTP(w, req2)
    var data map[string]string
    fmt.Println(w.Code)
    json.Unmarshal(w.Body.Bytes(), &data)
    fmt.Println(data)
    assert.Equal(t, http.StatusOK, w.Code)
    assert.Equal(t, data["Nickname"], name)
}

We have 2 data checker here, one is checking the BadRequest and another is checking normal request.
We can use go test -v test/* test all the test files under the test file.
Result:

$ go test -v test/*
=== RUN   TestGetUser
[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] GET    /user                     --> webserver/module.GetUser (3 handlers)
[GIN] 2023/01/03 - 16:54:25 | 400 |       203.5µs |                 | GET      "/user"
[GIN] 2023/01/03 - 16:54:25 | 200 |      31.084µs |                 | GET      "/user?name=Joe"
200
map[Nickname:Joe birthday:1990-01-01 gender: userID:113113]
--- PASS: TestGetUser (0.00s)
PASS
ok      command-line-arguments  0.584s

Let me modify the name Joe to Jo, this time, the test should be failed.

$ go test -v test/*
=== RUN   TestGetUser
[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] GET    /user                     --> webserver/module.GetUser (3 handlers)
[GIN] 2023/01/03 - 16:57:23 | 400 |     139.666µs |                 | GET      "/user"
[GIN] 2023/01/03 - 16:57:23 | 400 |       2.459µs |                 | GET      "/user?name=Jo"
400
map[]
    api_test.go:33: 
                Error Trace:    /webserver/test/api_test.go:33
                Error:          Not equal: 
                                expected: 200
                                actual  : 400
                Test:           TestGetUser
    api_test.go:34: 
                Error Trace:    /webserver/test/api_test.go:34
                Error:          Not equal: 
                                expected: ""
                                actual  : "Jo"
                            
                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1 +1 @@
                                -
                                +Jo
                Test:           TestGetUser
--- FAIL: TestGetUser (0.00s)
FAIL
FAIL    command-line-arguments  0.469s
FAIL

That means when we modified the universal code, the pipeline of test may fail and our codes need to be check.

CI-CD

Above, our test command is go test -v test/*.
So, when we need CI-CD, we only need to add this command to your .gitlab-ci.yaml file, maybe you can add a new stage named test.
If you are the first time hearing .gitlab-ci.yaml, here’s a reference for you: The .gitlab-ci.yml file