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 π?

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"
}
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)
}
}
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)
}
}
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)
}
}
}
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.
- https://github.com/golang/go/wiki/TableDrivenTests
- https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go
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) intThe len built-in function returns the length of v, according to its type:
String: the number of bytes in v.
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))
}
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},
}
π 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)
}
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
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)
}
}
}
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)
}
}
}
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.