All Posts All Posts

Practicing TDD Testing APIs in Gin

March 26, 2018·
Software Engineering
·4 min read
Tecker Yu
Tecker Yu
AI Native Cloud Engineer × Part-time Investor

If this article can make you put down your current courier and start becoming a TDD-first programmer from now on, then the purpose of this article has been achieved.

Why TDD is so important?

The most direct manifestation of TDD’s importance is No guessing and Robustness - everything is within the expectations of testing, ensuring that you have thoroughly considered all scenarios. Rather than directly throwing your code or API interfaces to testers and then continuing modifications based on feedback, this back-and-forth process actually reduces efficiency significantly. When we realize this, why not discover and fix most bugs ourselves through testing beforehand?

Secondly, as system scale continues to grow and new features are constantly added, unit tests help us perform system refactoring. We can locate dead code through code coverage, identify robust code by examining call counts to separate them into packages or modules for incremental compilation, and avoid bloating while accelerating build speeds in addressing system degradation issues. Testing is absolutely indispensable in these improvements.

TDD can affect code quality

Prioritizing testable classes or functions greatly influences your coding style. A TDD-first programmer should first consider: what are the dependencies (parameters) of this function? Object instances, basic types, or interfaces? What kind of function is easy to test? Here we must mention a famous quote from Clean Code:

Functions should do one thing. They should do it well and do it only.

When you’re willing to actively write tests, you increasingly hope these functions are like this,无形之中 telling you the shape of your target function. It sounds abstract, but every word is crucial. Simply put, building an engineering project is usually bottom-up. The more flexible the system, the finer the granularity should be - like building blocks, from the data layer that directly interacts with the database, to the persistence service layer, RESTful-based controller layer, validation and permission control, routing layer, logging management, monitoring, etc. Higher-level operations increasingly depend on lower-level operations, so the requirements for code robustness from top to bottom are higher. From a testing perspective, looking from bottom to top, the finer-grained functions at this level bear greater responsibility, and testing at this level is also the easiest to write because they are exactly the type of function that does only one thing.

TDD decreases BUGs

Consider these two controller functions:

func (ctrl *UserController) Register(user *entity.User) {
    // validate user
    // code ....
    // code ....
    // insert into DB
    // code ....
    // code ....
    // err handle
    // code ....
    // code ....
    // response
}
func (ctrl *UserController) Register(user *entity.User) {
    // ... validate.NewUser(user)
    // err handle ...
    // ... services.AddNewUser(user)
    // err handle ...
    // response ...
}

The first function doesn’t follow Do one thing, making the entire function body very bloated with more potential for bugs; the second controller function is very concise and basically doing one thing - handling errors and responding. If the validation layer and service layer functions are already tested, the possibility of problems in the controller decreases by another order of magnitude.

TDD is actually faster

Talk is cheap, show me the code.

Take an end-to-end test that most backend developers need to write, using a gin framework-built API as an example, POST json and then verify the returned json.

Preparation work:

func toReader(t interface{}) *bytes.Reader {
    b, _ := json.Marshal(t)
    return bytes.NewReader(b)
}

func toStruct(r *httptest.ResponseRecorder, t interface{}) error {
    resp := r.Result()
    body, _ := ioutil.ReadAll(resp.Body)
    return json.Unmarshal(body, t)
}

func makeJSONReq(method, path string, data interface{}) *http.Request {
    req, _ := http.NewRequest(method, path, toReader(data))
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Accept", "application/json")
    return req
}

These small utility functions can be encapsulated into test suites or toolkits.

Testing one of the API endpoints:

package api_test

import (
    "bytes"
    "encoding/json"
    "log"
    "net/http"
    "net/http/httptest"
    "os"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/yujiahaol68/rossy/app/model/checkpoint"

    "github.com/gin-gonic/gin"

    "github.com/yujiahaol68/rossy/app/database"
    "github.com/yujiahaol68/rossy/app/routers/api"
)

var router = gin.Default()

func setup() error {
    api.Router(router)
    return database.OpenForTest()
}

func TestMain(m *testing.M) {
    err := setup()
    if err != nil {
        log.Fatal(err)
        os.Exit(1)
    }
    defer database.Teardown()

    code := m.Run()
    os.Exit(code)
}

func Test_Source(t *testing.T) {
    t.Log("POST: /api/source")
    check := checkpoint.PostSource{"http://www.infoq.com/cn/feed", 2}

    w := httptest.NewRecorder()
    req := makeJSONReq("POST", "/api/source/", &check)
    router.ServeHTTP(w, req)

    var end endpoint.PostSource
    err := toStruct(w, &end)
    if err != nil {
        t.Fatal(err)
    }

    assert.Equal(t, http.StatusCreated, end.Code)
}

The actual test cases to write are very short. If using VSCode, you can run tests without leaving the editor. For larger systems, you don’t need to wait for all routes to be loaded before testing, significantly reducing startup waiting time.

Splitting the view layer into checkpoint and endpoint layers to represent the struct descriptions for input json and returned json respectively. For complex inputs, you can also consider mocking structs. There are many benefits - please start your TDD journey immediately!

More testing practices can be referenced from my example project.

Any mistakes would be greatly appreciated for correction.

Views