kpat.io
June 8, 2019

Testing with Gin

Cheers! This blog post is about writing integration tests for a Gin framework based application. Let’s dive right in to the basic setup1.

// file: main.go

package main

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

func main() {
    r := gin.Default()

    // register the ping endpoint
    r.GET("/ping", pingEndpoint)

    r.Run()
}

func pingEndpoint(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "pong",
    })
}

This returns a message on the endpoint /ping with the content

$ curl http://localhost:8080/ping
{"message": "pong"}

All other endpoints return a 404 error. In order to prepare for integration tests, the first thing to do with this is a small refactoring. The server setup can be extracted out of the main function context as such:

// file: main.go

package main

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

func main() {
    setupServer().Run()
}

// The engine with all endpoints is now extracted from the main function
func setupServer() *gin.Engine {
    r := gin.Default()

    r.GET("/ping", pingEndpoint)

    return r
}

func pingEndpoint(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "pong",
    })
}

Once the extraction is performed, main_test.go can be set up. The httptest package, that’s already baked into golang, was designed for this very use case. Fortunately the Gin framework developers have maintained compatibility with the standard http interfaces and can thus be used seamlessly in conjunction.

The following snipped shows a possible integration test for the /ping endpoint.

// file: main_test.go

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestPingRoute(t *testing.T) {
    // The setupServer method, that we previously refactored
    // is injected into a test server
    ts := httptest.NewServer(setupServer())
    // Shut down the server and block until all requests have gone through
    defer ts.Close()

    // Make a request to our server with the {base url}/ping
    resp, err := http.Get(fmt.Sprintf("%s/ping", ts.URL))

    if err != nil {
        t.Fatalf("Expected no error, got %v", err)
    }

    if resp.StatusCode != 200 {
        t.Fatalf("Expected status code 200, got %v", resp.StatusCode)
    }

    val, ok := resp.Header["Content-Type"]

    // Assert that the "content-type" header is actually set
    if !ok {
        t.Fatalf("Expected Content-Type header to be set")
    }

    // Assert that it was set as expected
    if val[0] != "application/json; charset=utf-8" {
        t.Fatalf("Expected \"application/json; charset=utf-8\", got %s", val[0])
    }
}

This is all there is to it code-wise. The rest of the magic is framework independent. As shown in the example, the http2 package can be used to fire request against your endpoints.

The following command can be used to execute the test

$ go test
[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    /ping                     --> test.pingEndpoint (3 handlers)
[GIN] 2018/06/08 - 10:40:06 | 200 |         103┬Ás |       127.0.0.1 | GET      /ping
PASS
ok      test      0.022s

Good Practices

Here are a few guidelines when doing integration tests together with a http framework:

  • Focus on one part of the system at a time.
    • Testing the database in conjunction with the http router is time consuming and results in fragile tests
    • Any other part of the system should be designed with testing in mind. Use interfaces to improve the overall testability of your code
  • The HTTP headers and status codes should be tested rigurously
  • Form validation errors are also a good place to start
  • It’s a good idea to use TDD to get jump-started writing testable code

References

  1. A quick start guide to set up the Gin framework
  2. The golang http package