DEV Community

enbis
enbis

Posted on

how to use build tags to control GO testing with a GitLab CI use case

abstract

The importance of testing code in programming is undoubtedly a fact. There cannot be good quality development without proper testing coverage. Sometimes, however, it is necessary to distinguish the tests or even launch them grouped together rather than all at once. This is the purpose of these few lines: present a valid solution for GO developers who wants to run their tests separately.

The use case I prepared involves a dummy GO project with test files. It has been pushed on a GitLab repository to run the Continuous Integration, while diversifying tests. This is the link to inspect the solution directly on GitLab.

prerequisites

  • basic knowledge of GO programming and testing
  • basic knowledge of the GitLab-CI

the GO project

Let's start our tour presenting the pilot GO project used to evaluate the usefulness of build tags. It is a simple Web Server with the sole purpose of handling http requests based on this URL query string /?value=10. Assuming the value is numeric, the web server returns a JSON payload with a success status and the value increased by one unit.

{"status":"success","result":"11"}
Enter fullscreen mode Exit fullscreen mode

In case of invalid request it returns JSON payload with a fail status and the error.

{"status":"fail","error":"strconv.Atoi: parsing \"10a\": invalid syntax"}
Enter fullscreen mode Exit fullscreen mode

the project layout

In this scenario, the GO project has two packages besides the main: handler and helper.

  • handler: decodes the http request and returns the answer after processing the data received
  • helper: exposes the functions to manipulate the data

the test files

The layout presented allows me to separate the context for the testing stage.
The handler can be tested using the net/http/httptest GO std library package. The handler_internal_test contains the httptest functions NewRequest and NewRecorder to prepare the http.Request and evaluate the http.Response. This could be identified as a sort of integration test.

func TestHandler(t *testing.T) {
    var response Response

    req := httptest.NewRequest("GET", "/?value=10", nil)
    w := httptest.NewRecorder()
    Handler(w, req)

    res := w.Result()
    defer res.Body.Close()

    assert.Equal(t, http.StatusOK, res.StatusCode)

    data, err := ioutil.ReadAll(res.Body)
    assert.NoError(t, err)

    err = json.Unmarshal(data, &response)
    assert.NoError(t, err)

    assert.Equal(t, "11", response.Result)
}
Enter fullscreen mode Exit fullscreen mode

The helper package contains the functions used to process the data. Those functions can be tested as a pure unit tests. This is the helper_internal_test created.

func TestConvert(t *testing.T) {
    tables := []struct {
        in  string
        out int
    }{
        {"1", 1},
        {"100", 100},
    }

    for _, table := range tables {
        converted, err := Convert(table.in)
        assert.NoError(t, err)
        assert.Equal(t, table.out, converted)
    }
}

func TestIncrement(t *testing.T) {
    tables := []struct {
        in  int
        out int
    }{
        {1, 2},
        {100, 101},
    }

    for _, table := range tables {
        converted := Increment(table.in)
        assert.Equal(t, table.out, converted)
    }
}

func TestToString(t *testing.T) {
    tables := []struct {
        in  int
        out string
    }{
        {1, "1"},
        {100, "100"},
    }

    for _, table := range tables {
        str := ToString(table.in)
        assert.Equal(t, table.out, str)
    }
}
Enter fullscreen mode Exit fullscreen mode

the build tags ( or build constraints )

What we produced so far is the Web Server and its test files. As a GO developer I just have to run the command go test ./... and my whole list of tests is executed.
The point is that I cannot distinguish between integration test and unit test or even have a separate report in a GitLab pipeline.
Here is where the build tags become useful. On top of that, they are easy to integrate into the code.
The syntax is a comment on top of the test file, with the special word +build followed by the keyword that identifies the tag. The constrains are injected into GO using -tags flag in the test command. For more details, here the doc.
Let's see what happens in practical terms:

  • integration test: the chosen keyword is integration, so I need to add the comment // +build integration on top of the handler_internal_test file and run the command go test ./... -tags=integration to execute it.
// +build integration

package handler

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"

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

func TestHandler(t *testing.T) {
....
....
Enter fullscreen mode Exit fullscreen mode
  • unit test: the keyword in this case is unit, I need to add the comment // +build unit on top of the helper_internal_test file and run the command go test ./... -tags=unit
// +build unit

package helper

import (
    "testing"

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

func TestConvert(t *testing.T) {
Enter fullscreen mode Exit fullscreen mode
  • unit and integration test: tags syntax allows to use AND and OR, as well as negative expressions. To run both at the same time, the command to use is go test ./... -tags=integration,unit

the GitLab CI pipeline

Last but not least, the integration of the build tags into the GitLab CI pipeline. In this case the purpose could be to see the result of the tests grouped by tags. The case presented here contains only two tags: integration and unit. The gitlab-ci.yml file will have two stages, each with the proper script.

stages:
  - unit_test
  - integration_test

unit:
  stage: unit_test
  script:
    - go test -v ./... -tags=unit

integration:
  stage: integration_test
  script:
    - go test -v ./... -tags=integration

Enter fullscreen mode Exit fullscreen mode

Which brings this result:

Alt Text

Thanks to the build tags in the GitLab pipeline presented above I'm able to distinguish tests by context, run them separately and inspect the result individually.

Discussion (0)