Go Testing

In the previous post in this series, we discussed how to write a microservice in Go. Now we are going to write some tests and make sure our code is doing what we think it should be doing.

Disclaimer: I'm not an expert in testing, any feedback on my approach is welcomed. This is meant to be a primer on uniting testing. The focus is on how to get started with the testing framework in Go because we all know how to test right πŸ™ˆ?

Door with a lock which doesn’t

Basic Go Testing

Go makes getting started with testing straight forward, with support built-in to the standard library and toolchain. We can call go test and it will compile and execute any function with the signature func TestXxx(*testing.T) in a file ending _test.go. Unlike other languages, Go expects test files live in the same directory as the code it is testing. This was an unusual approach in my experience as many other languages typically the testing code is shoved off somewhere to the side in a tests directory. However, I've come around to prefer this approach. I think it suggests the value to the test is the same as the value of the code - with good tests, it is normally worth more.

To start our journey off, let's write our first test for a function that returns foo.

package foo

func Foo() string {
    return "foo"
}

Run in the Go playground

As our function is meant to return foo our test is going to check that we received foo when we called Foo. To do that, we store the expected value and the actual value from Foo and then compare the two. This makes it easier to read the test and allows us to inspect the code easier with a debugger if we encounter an error.

package foo

import "testing"

func TestFoo(t *testing.T) {
    expected := "foo"
    actual := Foo()

    if actual != expected {
        t.Errorf("Expected '%s' but got '%s' instead", expected, actual)
    }
}

Run in the Go playground

That wasn't too bad. As the behaviour we are expecting from our Foo function s simple, our test code also reflects that. If we were testing a function which has a wider range of behaviours or outputs, our tests for it will need to also reflect that.

In this test called t.Errorf() instead of t.Error() to provide some extra details on the error. t.Errorf() allows us to us to use format strings to format the call and display values in a more useful manner, like fmt.Printf allows us to do. Calling t.Errorf() is essentially a wrapper around t.Logf() and t.Fail(). Similarly t.Error() is wrapper function around t.Log() and t.Fail().

If we needed to end the test immediately, we could also call t.Fatal() which is equivalent to calling t.Log() and then t.FailNow(). t.FailNow() marks the test as failed and calls runtime.Goexit. runtime.Goexit calls any deferred functions and exits the goroutine.

This is all there is to testing in Go. If a test doesn't call any of the Fail or Error functions, it is considered to have passed the test.

Testing StringService

In the previous post in this series, we created a simple service which provided two functions; Count which returned the length of a string and Uppercase which returned the string but in uppercase characters. Now we are going to add some tests to verify that they have behaviour we expected.

We wrote the service using Go Kit, which creates our service in layers, making it easier to test each part in isolation. We want to write tests for each layer so that we can test the smallest amount of code per test. This reduces the scope of behaviours we would expect from the code we are expecting. It also helps infer where any bugs might be, if a test is failing in a complicated test that has many levels of dependencies and calls out to other functions, it won’t be trivial to determine the cause. If you’re only testing a short and simple function, it should be easier to determine the issue.

Service Layer

Let’s start with testing Count, we will call it with the input string foo and expect it to return 3. As with our previous example, we will store the actual and expected results in variables and compare the two.

import "testing"

func TestCountFoo(t *testing.T) {
    svc := NewStringService()

    expected := 3
    actual := svc.Count("foo")

    if actual != expected {
        t.Errorf("Count('foo'): expected %d, actual %d",
            expected, actual)
    }
}

Run in the Go playground

All as expected, the result returned from Count is 3. Now we have our first test, let's try our code with some trickier strings.

Table Driven Tests

Instead of repeating the code we just wrote for our previous test, we are going to use Table Driven tests.

As the name suggests, we need to create a table of inputs and expected results for each of our tests. In the example below, we create a countTest which will store our input string and our expected result of the string. We iterate through the table comparing the expected result to the value returned by Count.

type countTest struct {
    s        string
    expected int
}

var countTests = []countTest{
    {"foo", 3},
    {"hello, world", 12},
    {`hello, δΈ–η•Œ`, 9},
}

func TestCount(t *testing.T) {
    svc := NewStringService()

    for _, tt := range countTests {
        actual := svc.Count(tt.s)

        if actual != tt.expected {
            t.Errorf("Count('%s'): expected %d, actual %d", tt.s,
                         tt.expected, actual)
        }
    }
}

Run in the Go playground

Table-driven testing is particularly useful if a function you are testing handles a wide variety of inputs by reduces the need to copy-pasta our code, providing more time to ponder our test cases. It is a simple and handy tool which you should use. Below are some links which go into more detail about Table Driven Tests.

Unicode

Running our Table Driven Tests, our third test case has failed due to Count returning 13 when called with the string β€œHello, δΈ–η•Œβ€, instead of 9. This is because our Count function returns the result of the builtin function len. So why is len returning the "wrong" answer?

func len(v Type) int

The len built-in function returns the length of v, according to its type:

String: the number of bytes in v.

-builtin - The Go Programming Language

Well, that isn't quite right, we want characters not bytes. Characters can be encoded in many different schemes longer than a single byte. Most applications and websites use the Unicode utf-8 encoding, Go's own source code must be in utf-8. Dealing with Unicode is one of the few topics every developer needs to know how to handle.

There has been a lot written on the topic by others who far better to explain. I first read about it in from Joel Spolky's The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

Coincidentally, utf-8 was co-created by Ken Thompson who also created Go. It isn't a surprise then that Go has a unicode/utf8 package that we can use. Now we can use RuneCountInString which will return the number of Unicode code points in a string.

package main

import (
    "fmt"
    "unicode/utf8"
)

func Count(s string) int {
    return utf8.RuneCountInString(s)
}

func main() {
    s := "Hello, δΈ–η•Œ"
    fmt.Println("bytes =", len(s))
    fmt.Println("runes =", Count(s))
}

Run in the Go Playground

Our test is passing again πŸŽ‰ and since everyone does (or should) love emoji, let's add some into our table for testing. Emoji are a good test for string handling since they are notorious for causing issues and are just fun.

var countTests = []countTest{
    {"foo", 3},
    {"hello, world", 12},
    {`hello, δΈ–η•Œ`, 9},
    {`Hello, δΈ–η•ŒπŸ––πŸΏπŸ³οΈβ€πŸŒˆ`, 11},
}

Run in the Go Playground

πŸ˜‚ just as expected our test with emoji has failed. β€œHello, δΈ–η•ŒπŸ––πŸΏπŸ³οΈβ€πŸŒˆβ€ is 11 characters long but our new version of Count is returning 15 runes. This is because our "πŸ–– (Raised Hand With Part Between Middle and Ring Fingers)" is modified with the "🏿"Emoji Modifier Fitzpatrick Type-6". Go's unicode/utf-8 counts these are two runes instead of 1 but that should only have resulted in use getting 12 from Count Where are the extra 3 runes coming from? It turns out that the "πŸ³οΈβ€πŸŒˆ Rainbow Flag" is actually "🏳️ Waving White Flag" a zero width joiner and "🌈 rainbow". "🏳️ Waving White Flag" is represented as "U+1F3F3" and "U+FE0F" together, so we get our 3 extra characters. Confused? Well don't worry, emoji and strings are hard

Getting the result I expected took a while to figure out, thankfully I fell upon a stackoverflow question which suggested using rivo/uniseg a module that implements Unicode Text Segmentation.

Unicode Text Segmentation according to Unicode Standard Annex #29 (http://unicode.org/reports/tr29/)."

Now using rivo/uniseg we can return the correct number of characters.

func (stringService) Count(s string) int {
    return uniseg.GraphemeClusterCount(s)
}

Run in the Go Playground

I additionally added some strings from the big list of naughty strings to check everything is behaving as expected.

Endpoints Layer

Our endpoint layer has two functions MakeCountEndpoint and MakeUppercaseEndpoint. These generate new endpoint.Endpoint for Count and Uppercase. We will need to check that MakeCountEndpoint and MakeUppercaseEndpoint return an endpoint which isn't Nil and return our expected values.

The endpoints for our service marshal data out of a request, call our service and the marshal the returned value into a response. Essentially, a thin wrapper around our service.

func MakeCountEndpoint(svc StringService) endpoint.Endpoint {
    return func(_ context.Context, request interface{}) (interface{}, error) {
        req := request.(countRequest)
        v := svc.Count(req.S)
        return countResponse{v}, nil
    }
}

The endpoint MakeCountEndpoint creates, makes a call to our stringService but we already have a test suite for our service layer and we want to test just our endpoint layer code. We will need to implement a mock StringService for this test suite.

Get Your Mocks Here

mock objects are simulated objects that mimic the behaviour of real objects in controlled ways

Wikipedia

Mock objects allow us to control the behaviour of our dependencies. By creating an object that implements the interface of our dependency (StringService), we can control everything that is returned from it including errors. Without mocks, we would be reliant on manipulating our StringService into producing an error as a side effect of some input. We wouldn't have the ability to simulate a layer producing incorrect results due to a bug unless we started putting bugs in our production code πŸ˜•.

Mocks can also be known as fakes or stubs. There doesn't seem to be any consistent naming to these. The main idea is that the simplest returns a predefined value and the most complex can replicate the production code behaviour with complex logic, assertions and exceptions.

For our tests, we need to return our expected values. We don't require any analyse or verification of inputs.

type mockSvc struct {
    StringService
    Response UppercaseResponse
}

func (m *mockSvc) Uppercase(s string) (string, error) {
    return m.Response.V, errors.New(m.Response.Err)
}

When we create a new mockSvc it has the expected response embedded into it. Thanks to Go, we can also embed the StringService interface, allowing us to skip implementing all the functions in the service we don't need.

For each of the tests in our test table, we define a request and a response. We can then compare our expected response to the actual response from our endpoint. Below you can see below the returned values from mockSvc.Uppercase, are the values from the expected response. Any differences when comparing the actual and expected responses will be due to an issue with our endpoints.

func TestUppercaseEndpoint(t *testing.T) {
    for _, tt := range makeUppercaseTests {
        svc := &mockSvc{
            Response: tt.Resp,
        }

        endpoint := MakeUppercaseEndpoint(svc)
        if endpoint == nil {
            t.Errorf("MakeUppercaseEndpoint: Didn't expect nil")
        }

        actual, err := endpoint(context.TODO(), tt.Req)
        actualResp := actual.(UppercaseResponse)
        if actualResp.Err != tt.Resp.Err {
            t.Errorf("endpoint: expected %v, actual %v",
                tt.Resp.Err, err)
        }

        if actual != tt.Resp {
            t.Errorf("MakeUppercaseEndpoint: expected %v, actual %v",
                tt.Resp, actual)
        }
    }
}

Run in the Go Playground

Our test code for MakeUppercaseEndpoint creates a mockSvc with the response we expect for each test case. We check that we have received an endpoint and not Nil as you cannot compare functions in Go. We then test Uppercase with our request, comparing the actual and expected responses.

Transport Layer

Our transport layer has two main functions DecodeCountRequest and DecodeUppercaseRequest. These functions take http.Request and return our request structs (CountRequest or UppercaseRequest). We will need to check that our requests are being converted from JSON in an http.Request to our request structs correctly.

For each of the tests in our test table, we define the request we are expecting. Below you can see that we convert our expected CountRequest to JSON. We then set our JSON to be the body for the new http.Request using http.NewRequest. We pass the http.Request to DecodeCountTests and compare the returned CountRequest to our expected one.

func TestDecodeCountRequest(t *testing.T) {
    for _, tt := range decodeCountTests {
        reqBytes, err := json.Marshal(tt.Req)
        if err != nil {
            t.Fatal(err)
        }

        httpReq, err := http.NewRequest("GET", "/Count", bytes.NewReader(reqBytes))
        if err != nil {
            t.Fatal(err)
        }

        requestInt, err := DecodeCountRequest(context.TODO(), httpReq)
        actual := requestInt.(CountRequest)
        if actual != tt.Req {
            t.Errorf("DecodeCountRequest: expected %v, actual %v",
                tt.Req, actual)
        }
    }
}

Run in the Go Playground

I wrote fewer tests for these layers as they contain little to no logic. They are mostly thin wrappers around the encoding/json module. These tests are more for ensuring that we catch any incompatible changes to the request and response structures defined in the endpoint layer.

We now have tests for each of the three layers of our code but have we covered every nook and cranny?

Test Coverage

We can produce a report for the coverage of our test over our production code using the builtin support in Go. We run our tests but add an extra flag to produce the coverage profile then we can view the results in a web browser.

go test -coverprofile cover.out
go tool cover -html=cover.out

Test coverage is only an indicator for which parts of our code has been run. It is important to remember that while a high percentage is good, it doesn't necessarily mean all the tests are also good. However, having no test for code provides no assurance that it is working by design.

PASS
coverage: 76.7% of statements
ok      github.com/neuralsandwich/how-to-microservice/go-kit-string-test
0.013s

Our testing results in a coverage score of 76.7%, which is good. If we have a look at the different file in the report, we can see that we lose some coverage for not testing the main.go file and we didn't test EncodeResponse in transport.go either. We could write some test for EncodeResponse but at that point, we are testing the standard library, which has better testing. We could chase a better score but I think we would have a little gain. Writing test can be a balance of knowing what to test and what not to. The aim is for the best bang for the buck.

Summary

This was a quick introduction to testing in Go. We wrote tests for each layer of our service, learnt about table-driven tests, utf-8, mock objects and how to produce coverage reports. Testing software is a big part of the engineering process in developing code. The how's and why's of testing is a science in itself but this StackExchange article is a good place to start that journey.

As always, I appreciate any feedback or if you want to reach out, I’m @neuralsandwich on twitter and most other places.