Go Test Your Code: An introduction to testing in Go

Photo by Fire Marshal John Erskine of Burnet Fire Department

As developers, we all know how important testing our code is. If you haven’t tested what you’ve written, you can’t really say you’ve created it. In the early stages of developing an app, we tend to manually test our code. But as more components and features are added, the code base will become large and unwieldy. Manually performing regression tests to ensure what you wrote one year ago still works after your latest merge yesterday, is a pain in the a**. Manual testing is simply not scalable.

Hence, we need to write code that will automate testing for us. It may be painful for us to write them initially, but subsequently, they will do the heavy lifting whenever we need to run regression tests. Now, if your app is written in Go, you might be wondering how to write tests for it. You’d be glad to know that testing in Go is pretty neat and simple. In this article, I’ll give you a solid introduction to testing in Go. Before we proceed, this article assumes that you already have a basic knowledge of Go, specifically functions and packages.


Let’s start off by creating our first test suite. Before we start, please be reminded that when I talk about any commands like go or taskkill , I am referring to running them from the CLI.

3 simple steps to create your test suite

Step 1: Create a new test file.

This file should have a name ending in _test.go (e.g. user_test.go ). The file can be added to any package you like. You may put it in the same package as the component to be tested (same folder, same package).

A benefit of placing test functions in the same package is that it allows us to access unexported functions and types, which enables white-box testing. These test files will only be built when go test command is called. They’re excluded from regular package builds triggered by the go run command.

However, you can also put it in a separate test package appended with _test if it helps you to be more organised. For example, if you wish to test your user login module in your users package, then declare your new test file in a new package named users_test (same folder, different package).

In fact, this is how we organise our test files at Rate. When go test is run, the test files in users_test package will be compiled separately from the users package. After compilation, they are linked to the main test binary.

Step 2: Write a test function.

This function should be written inside the file created in Step 1. The function name should be prefixed with Test , followed by a capitalised word, and should have the signature func TestXxx(t *testing.T).

Example:

func TestUserLogin(t *testing.T) {...}

Any functions that are not prefixed with Test (e.g. func UserBuilder), will not be treated as test cases. They will not be run when go test command is issued.

Step 3: Understand what you just made.

The user_test.go that you’ve just created can contain more than one test function, not only TestUserLogin. In other words, user_test.go can house many user related test cases. You can write other test functions like TestUserRegistration and TestUserPasswordReset within this same file to test user registration and password reset flows respectively. Hence, we call user_test.go a test suite. More specifically, our user test suite.

Also, the _test.go and func TestXxx(t *testing.T) are the 2 main conventions you need to remember when writing tests in Go. They are essential rules of testing in Go and utilise the pre-defined behaviour of the go test command. Now, we’re ready to run some tests.


How to run tests

The go test command is our go-to command for running tests. There are many ways in which you can run tests, and this is specified by supplying additional flags. The full list of flags are specified here.

First, I think it’ll be useful for you to understand the 2 modes of go test. They are local directory mode and package list mode.

Local directory mode is when go test is called inside a directory, without any package arguments supplied. Running go test in .../go/src/github.com/yourProject/model will compile and run all the tests in that directory (i.e. model package)

Package list mode is when go test is called with package/path arguments, such as go test users or go test ./testFiles/ .

For now, let’s focus on some variations of go test which you’re likely to use the most often.

  • go test ./…

This runs all test packages in this directory and any sub-directories. The ... is actually a wildcard expression in Go to denote all packages. When this command is run from the project root, it will run all your test packages.

  • go test -run [regex]

This runs all tests functions in the current directory that has a name matching the regex. This can be a quick way to run specific test cases, by supplying the test function name as the regex (e.g. go test -run TestUserLogin)

  • go test -v

This runs tests in verbose mode. Each test is printed to standard output when they are called. Any text output from t.Log() and t.Logf() will also be printed.

  • go test -cover

This runs tests and concurrently performs coverage analysis. When you use this flag in conjunction with go test ./... (i.e. go test ./... -cover ) and run it from your project root, it will output how much statement coverage you have for each package. An example output is shown below. The directory paths have been removed for security reasons.

Output of go test ./…

If you are using VSCode, the Go extension has the ability to generate the same output as above. You can do so by searching for “go coverage” in the Command Palette.

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

This command is an enhanced version of go test -cover . It renders a HTML page that visualises line-by-line coverage of each affected .go file. The first half ( go test -coverprofile=cover.out ) runs go test -cover under the hood and saves the output to the file cover.out . The second half ( go tool cover -html=cover.out ) generates a HTML page in a new browser window, based on the contents of cover.out .

When this command is run from the project root in conjunction with ./... (i.e. go test ./... -coverprofile=cover.out && go tool cover -html=cover.out ), the HTML presentation will contain all files affected. We can navigate between these files through the drop-down menu. An example is shown below.

Output of "go test ./... -coverprofile=cover.out && go tool cover -html=cover.out"

More info on Go’s test coverage capabilities can be found in here.


Tips and tricks for better testing

  1. Use open source packages.

The standard library is already quite powerful, with packages like testing , httptest and iotest . However, we can enhance our capabilities by using external packages built by other Go programmers. Some examples are:

  • testify — Simplifies the set-up and tear-down processes of our test suites whilst also making it easier for us to perform assertions.
  • go-spew— Helps to pretty print Go data structures like slices, maps and structs, which is very useful for debugging.

2. go test does not kill currently running Go processes.

In the even that you have another Go program running in the background, running go test will not terminate this program. The compilation and execution of the test binary takes place as an entirely different process. So, do not expect go test to serve as an automatic “kill switch” for you to transition from manual to automated testing.

As a side note, to kill any running Go programs triggered by go run , you will need to manually kill the process from the Terminal or Task Manager. If you are on Linux/Mac, Ctrl-C will do the job. If you are on Windows, you can try the taskkill command from Command Prompt/Powershell/Cygwin Bash.

If you’re on Cygwin, you can try using ps -W | grep YourProgramName.exe | awk '{print $1}' | xargs /bin/kill -f if you prefer bash commands. It does the same job as taskkill .

3. Understand the component you want to test.

Understand it well. We need to fully understand how the code is executed when a certain component is called. That means understanding how the statements and branches in that component are executed. This way, we would know how to test, what to test and what result to expect when we supply a certain input.

Another point of consideration is the importance of the component. If it is a crucial component, then we ought to do white-box testing to test all the paths and boundary cases.

4. Understand unit testing and integration testing.

We need to understand the pros and cons of both unit testing and integration testing, as well as when we should use them. This way, we’ll be better at designing our test suites.

Unit testing is very powerful because we can isolate and test a single component, but it does not let us test the logic leading to and following the usage of that component.

Integration testing allows us to test the entire flow, which is most representative of real world usage. However, our tests will take a longer time to run as a lot of logic will be redundantly executed for each test case.

5. Be mindful of URL redirects.

When we test our endpoints manually using a REST client (e.g. Postman, Insomnia), it will automatically “chase” HTTP redirects sent from the server by calling these redirected URLs. This makes it very convenient for us developers, as we can just call a single endpoint without needing to manually call other endpoints if the server replies with a HTTP 303 See Other .

However, if we’re looking to test our server endpoints, we need to check for redirects returned from these endpoints and call the redirected URLs manually. There is no predefined mechanism to “chase” redirects automatically because we’re essentially writing our own REST clients (i.e. our test suites) from scratch.


Go forth, and conquer!

Photo by Norwich Township Fire Department

Having read all of that, you can now start to write your own beautiful test suites. You’ll be more confident in developing new features now that your code base is well tested (hopefully). With the press of a few keystrokes and the magic of go test , you can ensure that everything you’ve written in the past, still works.


Related Readings

  1. TutorialsPoint — White box testing: https://www.tutorialspoint.com/software_testing_dictionary/white_box_testing.htm
  2. Command go — Test packages: https://golang.org/cmd/go/#hdr-Test_packages
  3. Command go — Testing flags: https://golang.org/cmd/go/#hdr-Testing_flags
  4. GURU99 — Statement Coverage in Software Testing: https://www.guru99.com/learn-statement-coverage.html
  5. The Go Blog — The cover story: https://blog.golang.org/cover
  6. GoDoc — package testify: https://github.com/stretchr/testify
  7. GoDoc — package spew: https://github.com/davecgh/go-spew

Rate Engineering

Tidbits from the Rate Engineering Team

Derian Pratama Tungka

Written by

I enjoy reading technology topics, and sometimes I write about them. https://www.linkedin.com/in/derianpt/

Rate Engineering

Tidbits from the Rate Engineering Team

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade