Golang : The Essential Guide to Unit Testing in Golang

Testing has always been a crucial part of software development, test cases to make sure your software works flawlessly

Testing has always been a crucial part of software development. Some programming languages are created without much focus on that aspect, but then there’s Golang.
The Google Team made sure testing is easily performed for apps and software written on Go. We like that. Dive into the review of Golang testing fundamentals as our QA professionals share a few tips on testing Go code from their personal and practical experience.

Why you should test your code?

Writing thousands of test cases to make sure your software works flawlessly is time-consuming. Due to that, proper and detailed testing is neglected during development and only carried out at the final stages.

This may end up as a problem for both the developers and the customer. Thousands of bugs, slow performance, and constant crashes are the most common resulting issues when testing is disregarded.

They can lead to a tech debt, and nobody wants that to happen. Avoiding these inconveniences is possible, but definitely requires your time and attention. Now, let’s figure out what can be done with the code you’ve written. Namely, go test!
. . .

GO Unit Testing

Unit Testing (UT) is a fundamental starting point which helps you gain an overall understanding of the processes if you are new to testing. It is probably the most effective way to search for all undesired elements in your code snippets while testing the overall output at the same time.

UT is considered time-consuming in general, but everything changed when Golang was introduced the world. Thanks to the package “testing” you can nail it in a couple of minutes.

1. Writing tests

Unit testing in Go is just as opinionated as any other aspect of the language like formatting or naming. The syntax deliberately avoids the use of assertions and leaves the responsibility for checking values and behaviour to the developer.

Here is an example of a method we want to test in the main package. We have defined an exported function called Sum which takes in two integers and adds them together.
package main func Sum(x int, y int) int { return x + y } func main() { Sum(5, 5) }

We then write our test in a separate file. The test file can be in a different package (and folder) or the same one (main). Here's a unit test to check addition:
package main import "testing" func TestSum(t *testing.T) { total := Sum(5, 5) if total != 10 { t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10) } }

Characteristics of a Golang test function:
  • The first and only parameter needs to be t *testing.T
  • It begins with the word Test followed by a word or phrase starting with a capital letter.
  • (usually the method under test i.e. TestValidateClient)
  • Calls t.Error or t.Fail to indicate a failure (I called t.Errorf to provide more details)
  • t.Log can be used to provide non-failing debug information
  • Must be saved in a file named something_test.go such as: addition_test.go
If you have code and tests in the same folder then you cannot execute your program with go run *.go. I tend to use go build to create a binary and then I run that.

You may be more used to using the Assert keyword to perform checking, but the authors of The Go Programming Language make some good arguments for Go's style over Assertions.

When using assertions:
  • tests can feel like they're written in a different language (RSpec/Mocha for instance)
  • errors can be cryptic "assert: 0 == 1"
  • pages of stack traces can be generated
  • tests stop executing after the first assert fails - masking patterns of failure
There are third-party libraries that replicate the feel of RSpec or Assert. See also stretchr/testify.

2. Test tables

The concept of "test tables" is a set (slice array) of test input and output values. Here is an example for the Sum function:
package main import "testing" func TestSum(t *testing.T) { tables := []struct { x int y int n int }{ {1, 1, 2}, {1, 2, 3}, {2, 2, 4}, {5, 2, 7}, } for _, table := range tables { total := Sum(table.x, table.y) if total != table.n { t.Errorf("Sum of (%d+%d) was incorrect, got: %d, want: %d.", table.x, table.y, total, table.n) } } }

If you want to trigger the errors to break the test then alter the Sum function to return x * y.
$ go test -v === RUN TestSum --- FAIL: TestSum (0.00s) table_test.go:19: Sum of (1+1) was incorrect, got: 1, want: 2. table_test.go:19: Sum of (1+2) was incorrect, got: 2, want: 3. table_test.go:19: Sum of (5+2) was incorrect, got: 10, want: 7. FAIL exit status 1 FAIL github.com/alexellis/t6 0.013s

3. Launching tests:

There are two ways to launch tests for a package. These methods work for unit tests and integration tests alike.
  1. Within the same directory as the test:
go test

This picks up any files matching packagename_test.go
or
  1. By fully-qualified package name
go test github.com/alexellis/golangbasics1

You have now run a unit test in Go, for a more verbose output type in go test -v and you will see the PASS/FAIL result of each test including any extra logging produced by t.Log.

The difference between unit and integration tests is that unit tests usually isolate dependencies that communicate with network, disk etc. Unit tests normally test only one thing such as a function.

4 More on go test

Statement coverage
The go test tool has built-in code-coverage for statements. To try it with out example above type in:
$ go test -cover PASS coverage: 50.0% of statements ok github.com/alexellis/golangbasics1 0.009s

High statement coverage is better than lower or no coverage, but metrics can be misleading. We want to make sure that we're not only executing statements, but that we're verifying behaviour and output values and raising errors for discrepancies.

If you delete the "if" statement from our previous test it will retain 50% test coverage but lose its usefulness in verifying the behaviour of the "Sum" method.

5 Generating an HTML coverage report

If you use the following two commands you can visualise which parts of your program have been covered by the tests and which statements are lacking:
go test -cover -coverprofile=c.out go tool cover -html=c.out -o coverage.html

Then open coverage.html in a web-browser.

6 Go doesn't ship your tests

In addition, it may feel un-natural to leave files named addition_test.go in the middle of your package. Rest assured that the Go compiler and linker will not ship your test files in any binaries it produces.

Here is an example of finding the production vs test code in the net/http package we used in the previous Golang basics tutorial.
$ go list -f={{.GoFiles}} net/http [client.go cookie.go doc.go filetransport.go fs.go h2_bundle.go header.go http.go jar.go method.go request.go response.go server.go sniff.go status.go transfer.go transport.go] $ go list -f={{.TestGoFiles}} net/http [cookie_test.go export_test.go filetransport_test.go header_test.go http_test.go proxy_test.go range_test.go readrequest_test.go requestwrite_test.go response_test.go responsewrite_test.go transfer_test.go transport_internal_test.go]

For more on the basics read the Golang testing docs.


7 Isolating dependencies

The key factor that defines a unit test is isolation from runtime-dependencies or collaborators.

This is achieved in Golang through interfaces, but if you're coming from a C# or Java background, they look a little different in Go. Interfaces are implied rather than enforced which means that concrete classes don't need to know about the interface ahead of time.

That means we can have very small interfaces such as io.ReadCloser which has only two methods made up of the Reader and Closer interfaces:
Read(p []byte) (n int, err error)
Reader interface
Close() error
Closer interface

If you are designing a package to be consumed by a third-party then it makes sense to design interfaces so that others can write unit tests to isolate your package when needed.

An interface can be substituted in a function call. So if we wanted to test this method, we'd just have to supply a fake / test-double class that implemented the Reader interface.
package main import ( "fmt" "io" ) type FakeReader struct { } func (FakeReader) Read(p []byte) (n int, err error) { // return an integer and error or nil } func ReadAllTheBytes(reader io.Reader) []byte { // read from the reader.. } func main() { fakeReader := FakeReader{} // You could create a method called SetFakeBytes which initialises canned data. fakeReader.SetFakeBytes([]byte("when called, return this data")) bytes := ReadAllTheBytes(fakeReader) fmt.Printf("%d bytes read.\n", len(bytes)) }

Before implementing your own abstractions (as above) it is a good idea to search the Golang docs to see if there is already something you can use. In the case above we could also use the standard library in the bytes package:
func NewReader(b []byte) *Reader

The Golang testing/iotest package provides some Reader implementations which are slow or which cause errors to be thrown half way through reading. These are ideal for resilience testing.

8 Worked example

I'm going to refactor the code example from the previous article where we found out how many astronauts were in space.

We'll start with the test file:
package main import "testing" type testWebRequest struct { } func (testWebRequest) FetchBytes(url string) []byte { return []byte(`{"number": 2}`) } func TestGetAstronauts(t *testing.T) { amount := GetAstronauts(testWebRequest{}) if amount != 1 { t.Errorf("People in space, got: %d, want: %d.", amount, 1) } }

I have an exported method called GetAstronauts which calls into a HTTP endpoint, reads the bytes from the result and then parses this into a struct and returns the integer in the "number" property.

My fake / test-double in the test only returns the bare minimum of JSON needed to satisfy the test, and to begin with I had it return a different number so that I knew the test worked. It's hard to be sure whether a test that passes first time has worked.

Here's the application code where we run our main function. The GetAstronauts function takes an interface as its first argument allowing us to isolate and abstract away any HTTP logic from this file and its import list.
package main import ( "encoding/json" "fmt" "log" ) func GetAstronauts(getWebRequest GetWebRequest) int { url := "http://api.open-notify.org/astros.json" bodyBytes := getWebRequest.FetchBytes(url) peopleResult := people{} jsonErr := json.Unmarshal(bodyBytes, &peopleResult) if jsonErr != nil { log.Fatal(jsonErr) } return peopleResult.Number } func main() { liveClient := LiveGetWebRequest{} number := GetAstronauts(liveClient) fmt.Println(number) }

The GetWebRequest interface specifies the following function:
type GetWebRequest interface { FetchBytes(url string) []byte }
Interfaces are inferred on rather than explicitly decorated onto a struct. This is different from languages like C# or Java.

The complete file named types.go looks like this and was extracted from the previous blog post:
package main import ( "io/ioutil" "log" "net/http" "time" ) type people struct { Number int `json:"number"` } type GetWebRequest interface { FetchBytes(url string) []byte } type LiveGetWebRequest struct { } func (LiveGetWebRequest) FetchBytes(url string) []byte { spaceClient := http.Client{ Timeout: time.Second * 2, // Maximum of 2 secs } req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { log.Fatal(err) } req.Header.Set("User-Agent", "spacecount-tutorial") res, getErr := spaceClient.Do(req) if getErr != nil { log.Fatal(getErr) } body, readErr := ioutil.ReadAll(res.Body) if readErr != nil { log.Fatal(readErr) } return body }

Choosing what to abstract
The above unit test is effectively only testing the json.Unmarshal function and our assumptions about what a valid HTTP response body would look like. This abstracting may be OK for our example, but our code coverage score will be low.

It is also possible to do lower level testing to make sure that the HTTP get timeout of 2 seconds is correctly enforced, or that we created a GET request instead of a POST.

Fortunately Go has set of helper functions for creating fake HTTP servers and clients.

Going further:
  • and refactor the test above to use a fake HTTP client.
  • what is the test coverage percentage like before and after?
. . .

GO BENCHMARK

Automated testing of packages is nice to learn as well. Benchmark tests provide you with reports on software performance. They will show you how fast the software loads. In case you don’t need to dive into details but want to conduct some simple code speed testing — use this command go test -bench=. To check the memory consumption and allocation you can use a –benchmem command.

Yes, Golang allows you just to write the word “Benchmark” before the method name, and it will be included in the testing report: func BenchmarkXxx(*testing.B). For more precise testing, create a benchmark for every function in your code.


INTEGRATION TESTING IN GO

With an eye to consistency, you can set-up integration tests in Go which allow you to write code faster without being distracted by separate Unit Tests.

Continuous integration checks the same issues as unit testing does but immediately after the code is submitted. This verifies that your code is logically compiled and every function performs like it is supposed to.

However, in the face of poor code coverage, this method has a downside though (we discuss this further down).

For complex cross-platform applications that are not limited to Go you can level-up your testing using frameworks like GoConvey. It supports native Golang “testing” package and brings more power to the overall process, allowing you to write behavioral tests in your IDE and check the results live in your browser.

For outer-connected services like DataBases you can opt for (guess what? drumroll..) Docker. Here you should pay attention to the DB migrations and make sure the created Docker containers are pulled up to the test cases.

SKIPPING IN GO

This testing technique is used for quicker and selective testing, in case you are in a hurry. You might disagree and say, “Why not simply creating a unit test for the specific function?” Of course, you can, but the thing is, it takes much more time.

When you are setting up testing for further integration — you don’t want to get distracted and create a totally separate doc to quickly run through a peculiar snippet. Instead, you can deliberately omit testing the functions you don’t want to check right now. This can be done with the help of go test -short.

Here’s the example of how you can implement this technique:
package mishmash
import ( “testing” )
func TestMishMash(t *testing.T) { if testing.Short() { t.Skip(“skipping test case in short mode”) } // the rest of test }

CODE COVERAGE IN GOLANG

So you’ve done a couple of unit tests, made sure everything works just fine, and you want to finish. The thing is, it may only appear to you that all of the code was tested. Lame. That’s what code coverage is for.

Run it through your code and check the percentage of really tested pieces in comparison to the ones that weren’t.

The most common feature of this testing technique is that we usually write a case template (this applies to integrated testing in particular) and then add something to the code that was not mentioned in this test case, and hence can’t be retrieved. Consequently, code coverage reports may disappoint you the first couple of times. But, in the end, you learn from your mistakes and improve your code coverage.

GO MORE

If you are feeling a bit lost while writing another test case it can be useful to check what go help test and go help testflag commands offer. Take a look at the “testing/iotest” and “testing/quick” packages.

GO CONCLUDE

The Golang Team showed us how testing can and should be done. Many programmers all over the world started to actually enjoy test-driven development thanks to Go. It was made to be simple — there’s barely an excuse not try it. We hope you will enjoy testing software on Golang as much as we do!

If you have an outstanding idea but don’t know how to turn it into a real app — contact us. Our teams of developers and QA experts are willing to provide you with a high-end product and thorough testing services.

Never miss a post from Chris Gregori, when you sign up for Ednsquare.