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"}
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"}
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)
}
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)
}
}
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 commandgo 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) {
....
....
- 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) {
- 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](https://gitlab.com/enbis/testex/-/blob/main/.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
Which brings this result:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aftj1522ugiwchoe29t6.png)
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.
Top comments (1)
What is the purpose of ... in the go test command? I've also seen that in go list but I'm not sure why it's necessary.