When I start a new project I begin by working on the interfaces like command line, services, plugins, etc. (the APIs). Once my API draft is good enough, I started writing tests. And I had to find out recently that Testing in Golang takes a little bit more attention than usual. Workarounds for bad design like mocking and monkey-patching are not readily available in Go like they are in dynamic languages. Many testing remedies are already available in Golang that promise to ease the pain but the general advice is not to use them. Instead one should make ones code testable and use the standard testing tools that are shipped with Golang itself. As always tools are a matter of taste and personal preferences - I wanted to do it in the recommended way.
I found out a few things that I wanted to share in this article. Due to my professional emphasis on testing I put a lot of attention on Testing Golang, too. In my experience this is works great since focus on testing fueled my learning process of the Golang language itself.
Using Interfaces
Consequently use interfaces in order to make your code testable. In Golang interfaces are a great way to separate responsibilities in your code base. They help to keep your code concise, focused on the task and make it testable, too.
Some excellent resources to get you started:
- http://nathanleclaire.com/blog/2015/10/10/interfaces-and-composition-for-effective-unit-testing-in-golang/
- http://nathanleclaire.com/blog/2015/03/09/youre-not-using-this-enough-part-one-go-interfaces/
Another great talk on the topic by Tomas Senart:
Dealing with os.Exit()
Library code should NEVER call os.Exit(). Instead return from functions using error. Output "error.Error()" and call "os.Exit()" from the calling main function! This is how other programmers expect your code to behave! Funny thing is that this dramatically simplifies the testing of the library code, too.
Dealing with the golang "fmt" package
Same rule as with os.Exit() applies for writing to stdout and stderr. If you are working on library code you need some way to inject these channels. Or leave writing to the channels to the main function. If you want to test the output itself there are ways, too:
- http://stackoverflow.com/questions/34462355/how-to-deal-with-the-fmt-golang-library-package-for-cli-testing/
Dealing with http services
I write my services as simple go functions that return result and error so I can test them like "normal" code. Then I use a wrapper to add the http capabilities. For the testing of the wrapper I use "net/http/httptest", an easy to use recorder to make sure my wrapper function behaves like expected. I also separated the Router() from the actual Webserver(). In this way I can test routes without running a server:
package gogrinder
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
time "github.com/finklabs/ttime"
)
...
func TestRouteGetStatistics(t *testing.T) {
// test with 3 measurements
fake := NewTest()
srv := TestServer{}
srv.test = fake
// put 3 measurements into the fake server
done := fake.Collect() // this needs a collector to unblock update
now := time.Now().UTC()
fake.Update("sth", 8*time.Millisecond, now)
fake.Update("sth", 10*time.Millisecond, now)
fake.Update("sth", 2*time.Millisecond, now)
close(fake.measurements)
<-done
// invoke REST service
req, _ := http.NewRequest("GET", "/statistics", nil)
rsp := httptest.NewRecorder()
// ==> using Router() for testing routes!
srv.Router().ServeHTTP(rsp, req)
if rsp.Code != http.StatusOK {
t.Fatalf("Status code expected: %s but was: %v", "200", rsp.Code)
}
body := rsp.Body.String()
if body != fmt.Sprintf(`{"results":[{"testcase":"sth","avg":6666666,"min":2000000,` +
`"max":10000000,"count":3,"last":"%s"}],"running":false}`, now.Format(ISO8601)) {
t.Fatalf("Response not as expected: %s", body)
}
}
...
Dealing with the golang "time" package (careful, I beat my own drum here!)
If your code runs concurrently like for example a simulation engine, then you should definitely test that. Most simulations run for hours so time is a limiting factor for testing. Instead of testing your code in real time you want to "squeeze" time so the tests run fast. Golang has a mechanism built in to deal with that but unfortunately it is not open to the public (https://github.com/golang/go/issues/13788). Until this is fixed you need to use a custom 3rd party fake clock. Unfortunately these fake clocks have issues and that is why there are so many of them. Two fake clocks that I used and that work reliably in some of the use cases:
- https://github.com/pivotal-golang/clock
- https://github.com/finklabs/ttime
Use Continuous Integration and Code Coverage
I use Drone.IO to run test for my open source code (e.g. https://drone.io/github.com/finklabs/GoGrinder/latest). If you scroll down you see a complete code coverage report for both the Golang code and the Angularjs frontend tests. You can of course do that on your developer machine ("$ gocov test | gocov report") but I want to have it automatically done on each commit and for everybody to see that my code is clean and dandy.
I you host your code on github, or bitbucket then all of this is ridiculously easy to setup. Just setup an account at Drone.IO, select your repo, set language "Go1" and add the following commands:
# install gogrinder dependencies
go get ./...
# install test dependencies
go get golang.org/x/tools/cmd/cover
go get -v github.com/axw/gocov
go install github.com/axw/gocov/gocov
# install nodejs requirements
npm -d install
./node_modules/bower/bin/bower install
# run the tests on the go code
gocov test | gocov report
# run the tests on the Angularjs frontend
npm test
That is what I currently have to say about Testing in Golang.
Best, Mark
Resources
- https://golang.org/pkg/testing/
- https://drone.io/