Testing Your Go App: Get Started the Right Way
Testing frameworks such as Testify, Ginkgo, and Gomega can streamline testing for Golang applications, however, some of the most efficient and effective tools for testing can be found in Go’s standard libraries. In this article, Toptal developer Gabriel Aszalos covers the basics of testing in Go, from table testing to JSON response assertion.
Testing frameworks such as Testify, Ginkgo, and Gomega can streamline testing for Golang applications, however, some of the most efficient and effective tools for testing can be found in Go’s standard libraries. In this article, Toptal developer Gabriel Aszalos covers the basics of testing in Go, from table testing to JSON response assertion.
Gabriel is a software engineer who has developed a versatile array of applications for leading corporations like Thomson Reuters, LexisNexis, and Vodafone. He has experience with a variety of programming languages, including JavaScript, Angular, React, and Node.js, but his passion is for developing with Go, and he has become a regular conference speaker and open-source contributor on the topic.
Expertise
Previous Role
Full-stack DeveloperPREVIOUSLY AT
If you’re fairly new to Go and are coming from languages such as JavaScript or Ruby, you are likely accustomed to using existing frameworks that help you mock, assert, and do other testing wizardry. While there are a handful of testing frameworks available, including Testify, Ginkgo, and Gomega, Go’s standard libraries provide a versatile set of testing tools that let you eliminate dependence on external resources.
Testing was the first impediment I stumbled upon when learning this remarkable programming language a couple of years ago, a time when there were far fewer resources available. I now know that testing success in Go means traveling light on dependencies (as with all things Go), relying minimally on external libraries, and writing good re-usable code. This presentation of Blake Mizerany’s experiences venturing forth with third party testing libraries is a great start to adjusting your mindset. You will see some good arguments about using external libraries and frameworks versus doing it “the Go way”.
It may seem counter-intuitive to build your own testing framework and mocking concepts, but is easier than one would think, and a good starting point for learning the language. Plus, unlike when I was learning, you have this article to guide you through common testing scenarios as well as introduce techniques that I consider best practices for efficiently testing and keeping code clean.
Table Testing in Go
The basic testing unit - of ‘unit testing’ fame - can be any component of a program in its simplest form which takes an input and returns an output. Let’s take a look at a simple function we’d like to write tests for. It is nowhere near perfect or complete, but it’s good enough for demonstration purposes:
avg.go
func Avg(nos ...int) int {
sum := 0
for _, n := range nos {
sum += n
}
if sum == 0 {
return 0
}
return sum / len(nos)
}
The function above, func Avg(nos ...int)
, returns either zero or the integer average of a series of numbers that are given to it. Now let’s write a test for it.
In Go, it is considered best practice to name a test file with the same name as the file which contains the code being tested, with the added suffix _test
. For example, the above code is in a file named avg.go
, so our test file will be named avg_test.go
.
Note that these examples are only excerpts of actual files, as the package definition and imports have been omitted for simplicity.
Here’s a test for the Avg
function:
avg__test.go
func TestAvg(t *testing.T) {
for _, tt := range []struct {
Nos []int
Result int
}{
{Nos: []int{2, 4}, Result: 3},
{Nos: []int{1, 2, 5}, Result: 2},
{Nos: []int{1}, Result: 1},
{Nos: []int{}, Result: 0},
{Nos: []int{2, -2}, Result: 0},
} {
if avg := Average(tt.Nos...); avg != tt.Result {
t.Fatalf("expected average of %v to be %d, got %d\n", tt.Nos, tt.Result, avg)
}
}
}
There are several things to note about the function definition:
- First, the prefix of ‘Test’ on the test function name. This is necessary so that the tool will pick it up as a valid test.
- The latter part of the function name is generally the name of the function or method being tested, in this case
Avg
. - We also need to pass in the testing structure called
testing.T
, which allows for control of the test’s flow. For more details on this API, please visit the documentation page.
Now let’s talk about the form in which the example is written. A test suite (a series of tests) is being run through the function Avg()
, and each test contains a specific input and the expected output. In our case, each test sends in a slice of integers (Nos
) and expects a specific return value (Result
).
Golang Interface Mocking
One of the greatest and most powerful feature that the Go language has to offer is called an interface. Besides the power and flexibility that we get from interfacing when architecting our programs, interfacing also gives us amazing opportunities to decouple our components and test them thoroughly at their meeting point.
Let’s take an imaginary scenario where we need to read the first N bytes from an io.Reader and return them as a string. It would look something like this:
readn.go
// readN reads at most n bytes from r and returns them as a string.
func readN(r io.Reader, n int) (string, error) {
buf := make([]byte, n)
m, err := r.Read(buf)
if err != nil {
return "", err
}
return string(buf[:m]), nil
}
Obviously, the main thing to test is that the function readN
, when given various input, returns the correct output. This can be done with table testing. But there are two other non-trivial aspects we should cover, which are checking that:
-
r.Read
is called with a buffer of size n. -
r.Read
returns an error if one is thrown.
In order to know the size of the buffer that is passed to r.Read
, as well as take control of the error that it returns, we need to mock the r
being passed to readN
. If we look at the Go documentation on type Reader, we see what io.Reader
looks like:
type Reader interface {
Read(p []byte) (n int, err error)
}
That seems rather easy. All we have to do in order to satisfy io.Reader
is have our mock own a Read
method. So our ReaderMock
can be as follows:
type ReaderMock struct {
ReadMock func([]byte) (int, error)
}
func (m ReaderMock) Read(p []byte) (int, error) {
return m.ReadMock(p)
}
Let’s analyze the above code for a little bit. Any instance of ReaderMock
clearly satisfies the io.Reader
interface because it implements the necessary Read
method. Our mock also contains the field ReadMock
, allowing us to set the exact behavior of the mocked method, which makes it super easy for us to dynamically instantiate whatever we need.
A great memory-free trick for ensuring that the interface is satisfied at run time is to insert the following into our code:
var _ io.Reader = (*MockReader)(nil)
This checks the assertion but doesn’t allocate anything, which lets us make sure that the interface is correctly implemented at compile time, before the program actually runs into any functionality using it. An optional trick, but helpful.
Moving on, let’s write our first test, in which r.Read
is called with a buffer of size n
. To do this, we use our ReaderMock
as follows:
func TestReadN_bufSize(t *testing.T) {
total := 0
mr := &MockReader{func(b []byte) (int, error) {
total = len(b)
return 0, nil
}}
readN(mr, 5)
if total != 5 {
t.Fatalf("expected 5, got %d", total)
}
}
As you can see above, we’ve defined the behavior for the Read
function of our “fake” io.Reader
with a scope variable, which can be later used to assert the validity of our test. Easy enough.
Let’s look at the second scenario we need to test, which requires us to mock Read
to return an error:
func TestReadN_error(t *testing.T) {
expect := errors.New("some non-nil error")
mr := &MockReader{func(b []byte) (int, error) {
return 0, expect
}}
_, err := readN(mr, 5)
if err != expect {
t.Fatal("expected error")
}
}
In the above testing, any call to mr.Read
(our mocked Reader) will return the defined error, thus it is safe to assume that the correct functioning of readN
will do the same.
Function Mocking with Go
It isn’t often that we need to mock a function, because we tend to use structures and interfaces instead. These are easier to control, but occasionally we can bump into this necessity, and I frequently see confusion around the topic. Some people have even asked how to mock things like log.Println
. Although it is rarely the case that we need to test input given to log.Println
, we will use this opportunity to demonstrate.
Consider this simple if
statement below that logs output depending on the value of n
:
func printSize(n int) {
if n < 10 {
log.Println("SMALL")
} else {
log.Println("LARGE")
}
}
In the above example, we assume the ridiculous scenario where we specifically test that log.Println
is called with the correct values. In order for us to mock this function, we have to wrap it inside our own first:
var show = func(v ...interface{}) {
log.Println(v...)
}
Declaring the function in this manner - as a variable - allows us to overwrite it in our tests and assign whatever behavior we want to it. Implicitly, lines referring to log.Println
are replaced with show
, so our program becomes:
func printSize(n int) {
if n < 10 {
show("SMALL")
} else {
show("LARGE")
}
}
Now we can test:
func TestPrintSize(t *testing.T) {
var got string
oldShow := show
show = func(v ...interface{}) {
if len(v) != 1 {
t.Fatalf("expected show to be called with 1 param, got %d", len(v))
}
var ok bool
got, ok = v[0].(string)
if !ok {
t.Fatal("expected show to be called with a string")
}
}
for _, tt := range []struct{
N int
Out string
}{
{2, "SMALL"},
{3, "SMALL"},
{9, "SMALL"},
{10, "LARGE"},
{11, "LARGE"},
{100, "LARGE"},
} {
got = ""
printSize(tt.N)
if got != tt.Out {
t.Fatalf("on %d, expected '%s', got '%s'\n", tt.N, tt.Out, got)
}
}
// careful though, we must not forget to restore it to its original value
// before finishing the test, or it might interfere with other tests in our
// suite, giving us unexpected and hard to trace behavior.
show = oldShow
}
Our takeaway shouldn’t be ‘mock log.Println
’, but that in those very occasional scenarios when we do need to mock a package-level function for legitimate reasons, the only way to do so (as far as I am aware) is by declaring it as a package-level variable so that we can take control of its value.
However, if we ever do need to mock things like log.Println
, a much more elegant solution can be written if we were to use a custom logger.
Go Template Rendering Tests
Another fairly common scenario is to test that the output of a rendered template is according to expectations. Let’s consider a GET request to http://localhost:3999/welcome?name=Frank
, which returns the following body:
<html>
<head><title>Welcome page</title></head>
<body>
<h1 class="header-name">
Welcome <span class="name">Frank</span>!
</h1>
</body>
</html>
In case it wasn’t obvious enough by now, it’s not a coincidence that the query parameter name
matches the content of the span
classed as “name”. In this case, the obvious test would be to verify that this happens correctly every time across multiple outputs. I found the GoQuery library to be immensely helpful here.
Now we can write our test in this manner:
welcome__test.go
func TestWelcome_name(t *testing.T) {
resp, err := http.Get("http://localhost:3999/welcome?name=Frank")
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
doc, err := goquery.NewDocumentFromResponse(resp)
if err != nil {
t.Fatal(err)
}
if v := doc.Find("h1.header-name span.name").Text(); v != "Frank" {
t.Fatalf("expected markup to contain 'Frank', got '%s'", v)
}
}
First, we check that the response code was 200/OK before proceeding.
I believe that it’s not too far-fetched to assume that the rest of the code snippet above is self-explanatory: we retrieve the URL using the http
package and create a new goquery-compatible document from the response, which we then use to query the DOM that was returned. We check that the span.name
inside h1.header-name
encapsulates the text ‘Frank’.
Testing JSON APIs
Go is frequently used to write APIs of some sort, so last but not least, let’s look into some high-level ways of testing JSON APIs.
Consider if the endpoint previously returned JSON instead of HTML, so from http://localhost:3999/welcome.json?name=Frank
we would expect the response body to look something like:
{"Salutation": "Hello Frank!"}
Asserting JSON responses, as one might have already guessed, is not much different from asserting template responses, with the exception that we don’t need any external libraries or dependencies. Go’s standard libraries are sufficient. Here is our test confirming that the correct JSON is returned for the given parameters:
welcome__test.go
func TestWelcome_name_JSON(t *testing.T) {
resp, err := http.Get("http://localhost:3999/welcome.json?name=Frank")
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var dst struct{ Salutation string }
if err := json.NewDecoder(resp.Body).Decode(&dst); err != nil {
t.Fatal(err)
}
if dst.Salutation != "Hello Frank!" {
t.Fatalf("expected 'Hello Frank!', got '%s'", dst.Salutation)
}
}
If anything other than the structure that we decode against would be returned, json.NewDecoder
will instead return an error and the test will fail. Considering that the response decodes against the structure successfully, we check that the contents of the field are as expected - in our case “Hello Frank!”.
Setup & Teardown
Testing with Go is easy, but there is one problem with both the JSON test above and the template rendering test before that. They both assume that the server is running, and this creates an unreliable dependency. Also, it’s not a great idea to go against a “live” server.
Luckily, Go offers the httptest package to create test servers. Tests spark up their own separate server, independent from our main one, and so testing won’t interfere with production.
In these cases it’s ideal to create generic setup
and teardown
functions to be called by all tests requiring a running server. Following this new, safer pattern, our tests would end up looking something like this:
func setup() *httptest.Server {
return httptest.NewServer(app.Handler())
}
func teardown(s *httptest.Server) {
s.Close()
}
func TestWelcome_name(t *testing.T) {
srv := setup()
url := fmt.Sprintf("%s/welcome.json?name=Frank", srv.URL)
resp, err := http.Get(url)
// verify errors & run assertions as usual
teardown(srv)
}
Note the app.Handler()
reference. This is a best practice function that returns the application’s http.Handler, which can instantiate either your production server or a test server.
Conclusion
Testing in Go is a great opportunity to assume the outer perspective of your program and take on the shoes of your visitors, or in most cases, the users of your API. It provides the great opportunity to make sure you are both delivering good code and a quality experience.
Whenever you’re unsure of the more complex functionalities in your code, testing comes in handy as a reassurance, and also guarantees that the pieces will continue to work together well when modifying parts of larger systems.
I hope this article was of use to you, and you’re welcome to comment if you know of any other testing tricks.
Further Reading on the Toptal Blog:
Ulm, Baden-Württemberg, Germany
Member since April 28, 2016
About the author
Gabriel is a software engineer who has developed a versatile array of applications for leading corporations like Thomson Reuters, LexisNexis, and Vodafone. He has experience with a variety of programming languages, including JavaScript, Angular, React, and Node.js, but his passion is for developing with Go, and he has become a regular conference speaker and open-source contributor on the topic.
Expertise
Previous Role
Full-stack DeveloperPREVIOUSLY AT