Unit Testing made easy in Go

In this article, we will learn about unit testing in Go. Go provide built-in functionality to test your Go code. Hence, we don’t need an expensive setup or 3rd party libraries to create unit tests.

Uday Hiwarale
May 11 · 13 min read
(source: pexels.com)

Like cells are the building units of our body, likewise, unit components make up a software. Similarly, as the functioning of our body depends on the absolute efficiency and reliability of these cells, efficiency, and reliability of a software depends on efficiency, and reliability of the unit components that makes it up.

So what are these unit components? They can be functions, structs, methods and pretty much anything that end user might depend on. Hence, we need to make sure, whatever the inputs to these unit components are, they should never break the application.

So how we can test the integrity of these unit components? By creating unit tests. A unit test is a program that tests a unit component by all possible means and compares the result to the expected output.

So what can we test? If we have a module or a package, we can test whatever exports are available in the package (because they will be consumed by the end user). If we have an executable package, whatever units we have available within package scope, we should test it.


Let’s see how a unit test looks like in Go.

import "testing"func TestAbc(t *testing.T) {
t.Error() // to indicate test failed
}

This is the basic structure of a unit test in Go. The built-in testing package is provided by the Go’s standard library. A unit test is a function which accepts the argument of type *testing.T and calls the Error (or any other error methods which we will see later) on it. This function must start with Test keyword and the latter name should start with an uppercase letter (for example, TestMultiply and not Testmultiply).

Let’s first work old fashioned executable package in $GOPATH. I have created a greeting package which we will use to print some greeting messages.

In greeting package, we have two files. hello.go provides a hello function which accepts a user string and returns a Hello greeting message. Inside main.go file, which is the entry point of the program execution, we consume hello function and outputs the result to the console. We have used aurora package to print colorful texts to the console.

To execute the program, we need to use go run *.go because hello function is provided from the hello.go file. Hence, we need to run all the files at once in order to make our program work. Read my tutorial on Packages in Go to know more about how packages work.

As of now, we are confident that our hello function will work fine in any conditions. But as legends say, if something can happen, will happen. And that is what if somebody passes an empty string to hello function? We need to handle this case by returning a default message. Let’s do it.

When the user provides an empty user argument, we return greeting message with Dude as the default user. So far so good, but hello function could have been more complicated and it’s not a good practice to test all functionalities of unit components inside main function. It’s time to create a unit test for hello function to test it with all possible arguments.

In Go, you save unit tests inside separate files with a filename ending with _test.go. Go provides go test command out of the box which executes these files and runs tests. Let’s create a test function for our hello function.

go test

We have created hello_test.go file to test hello function inside hello.go. As, usual we have created a test function as explained earlier. You can have as many test functions as you want inside a single test file as you want. The collection of test cases (functions) is called a test suit.

Inside TestHello test function, we called hello function with some arguments and check if results are as expected. If the result of execution is not valid, you should call t.Error or t.Fail or t.Errorf indicating that there was an error in the test.

To execute all the test in the current package, use command go test. Go will run all the test files along with other files except main function. If there testing.T does not encounter any errors, then all the test are passed and Go will output test results with package name and time it took in seconds to execute all tests for that given package.

$ greeting go test
PASS
ok greeting 0.006s

You can output print additional information about test function using verbose -v command argument (flag).

go test with verbose mode

We can log additional information in verbose mode using t.Log or t.Logf method as shown below.

go test with verbose additional logs

So far our test passed but what if a test fails? Let’s modify our hello function and return greeting message without punctuation when the input argument is not empty. This genuinely looks like a mistake we could make while writing hello function. Let’s create two tests, one for empty argument and one for a valid argument.

test failed

If you need to colorize your test outputs, for example, green color for the passed test and red color for failed tests, you can use gotest package (install using go get -u github.com/rakyll/gotest command).

use gotest instead of go test command with all valid options

If you have lots of test files with test functions but you want to selectively run few, you can use -run flag to match test functions with their name.

run test functions starting with the name TestHelloE

You can specify the selected test files to run but in that case, Go will not include other files into the compilation which might be needed by the test. Hence, you need to specify dependency files as well.

go test with the selected test files

Test Coverage

Test Coverage is the percentage of your code covered by test suit. In layman’s language, it is the measurement of how many lines of code in your package were executed when you ran your test suit (compared to total lines in your code). Go provide built-in functionality to check your code coverage.

go test with coverage

In the above example, we have TestHello test function which tests hello function for the case of non-empty argument. I have removed main.go to make our package non-executable (also changed Hello function case).

To see the code coverage, we use -cover flag with go test command. Here, our test passes but the code coverage is only 66.7%. This means only 66.7% of our code was executed by the test. We need to see what did we miss to cover in the test.

Go provide an additional -coverprofile flag which is used to output information about coverage information a file. With this flag, we don’t need to use -cover flag as it is redundant.

go test with cover profile

In the above example, we extracted coverage results into cover.txt file. Here extension .txt of the file is not important, it can be whatever you want. This coverage profile file can be used to see which parts of the code was not covered by the test. Go provides cover tool (out of many built-in tools) to analyze coverage information of a test. We use this tool to accept our coverage profile and outputs an HTML file which contains the human-readable information about the test in a very interactive format.

go cover tool

We invoke the cover tool using go tool cover command. We instruct cover tool to output HTML code using -html flag which accepts a coverage profile. -o flag is used to output this information to a file. In the above example, we have created a cover.html file which contains coverage information.

If you open cover.html file inside a browser, you can clearly see which part of the code is not covered by the test. In the above example, the code in red color is not covered by the test. This proves that we haven’t written a test case where Hello function received an empty string as an argument.


Working with tests in Go Module

What we have done so far is to create test cases for package located inside $GOPATH. Post Go1.11, we have Go modules which are favored. In the previous tutorial, we have created a module to manipulate numbers. Let’s carry it forward and write test cases for it.

go test for a package in the module

In our calc package, we have created a test file with the name math_test.go which contains the test function TestMathAdd to validate Add function. So far we are familiar with this but when we want to run tests for a package inside a Go Module, you can either go inside the package directory and run command go test or use the relative path to the package directory as shown above.

Multiple Inputs Problem

A unit component is more reliable when tested with more data. So far, we have tested our unit components with one input data, but in reality, we should test them with sufficiently reliable data.

The best approach is to create an array of input data & expected result and run tests with each element (data) of the array (using for loop). The best type to hold data of different types is a struct (read more about structs).

In the above example, we have defined InputDataItem struct type which holds input numbers to Add function (inputs field), the expected result (result field) and if Add function returns an error (hasError field).

Inside our TestMathAdd function, we are created dataItems which is an array of InputDataItem. Using for loop, we iterated dataItems and tested if Add function returns an expected output.

There is nothing more complicated than this when it comes to testing a package. But as our calc package a part of nummanip module, and a module can have multiple packages. How can we test all the packages at once?

Let’s create another transform package in our nummanip module.

go test transform package

In the above example, we created square.go file inside transform package which contains SquareSlice function. This function accepts a slice of integers as an argument and returns a slice with the square of these numbers. Inside square_test.go file, we have created TestTransformSquare which tests for the equality of expectedResult slice and result slice. We have used reflect built-in package to checked for this quality using DeepEqual function. So far, when we tested tranform package using go test ./tranform command and our test passed.

So far, we have tested each package separately. To test all the packages in a module, you can use go test ./... command in which ./... matches all the packages in the module.

go test ./…

go test ./... command goes through each package and runs the test files. You can use all the command line flags like -v or -cover as usual. As you can see in the results above, we got the 100% coverage of our code and all tests passed the test. But what is that (cached) string in the test result message instead of the execution time of the tests?

There are two ways to run tests, first is local directory mode where we run test using go test which is what we used when our greeting package was inside $GOPATH. The second way is to run tests in the package list mode. This mode is activated when we list what packages to test, for example,

  • go test . to test package in the current directory.
  • go test package when package belongs to $GOPATH as long as you are not executing this command from inside a Go Module.
  • go test ./tranform to test package in ./tranform directory.
  • go test ./... to test all the package in the current directory.

In package list mode, Go caches the only successful test results to avoid repeated running of the same tests. Whenever Go run tests on a package, Go creates a test binary and runs it. You can output this binary using -c flag with go test command. This will output .test file but won’t run it. Additionally, rename this file using -o flag.

output test binary with -c flag

In my findings, even if there is a change in the source code of the test function, Go will reuse the results from the cache if there is no change in the test binary. For example, renaming of a variable won’t change the binary.

There is no valid reason to override cache because it can save you precious time. But if you want to see the execution time, you can use -count=1 which will run the test exactly one time and ignore the cache. You can also use go clean -testcache {path-to-package(s)}. If you want to disable caching globally, you can set GOCACHE environment variable to off.


Separation of concern

When we write test cases inside a package, these test files are exposed to exported and non-exported members of the package (because they are in the same directory of the source files). So we are not sure, not whether or not these unit components are accessible to the end user.

To avoid this, we can change the package name inside the test file and prefix it with _test. This way, our test files belongs to a different package (but still they are in the same directory of the package we are testing). Since now the test files belong to the different package, we need to import the package we are testing and access the unit components using . (dot) operator.

putting tests in a different package

In the above example, we changed the package name of the test file to transform_test and accessed the SquareSlice function from this package import. Since SquareSlice is exported (because of the uppercase), it works fine but if we would have missed the uppercase and written squareSlice, it wouldn’t have been exported and our test would have failed.


Test Data

Let’s say that you have a package that performs some operations on CSV or Excel spreadsheet files, and you want to write test cases for it, where would you store the file? You can’t store outside the package because then you would need to ship them separately for anybody who wants to perform tests.

Go recommends creating a testdata directory inside your package. This directory is ignored when you run or build your package using standard go command. Inside your test file, you can access this directory using built-in OS package, a sample of accessing a file from testdata is shown below

// some_test.gofile, err := os.Open("./testdata/file.go")
if err != nil {
log.Fatal(err)
}

Using Assertions

If you are familiar with tests in other languages like node.js then you probably have used assertion libraries like chai or built-in package assert. Go does not provide any built-in package for assertions.

In the official FAQs documentation, this is what spec developers say

Go doesn't provide assertions. They are undeniably convenient, but our experience has been that programmers use them as a crutch to avoid thinking about proper error handling and reporting. [more]

In nutshell, Go want developers to write the logic to test the results of the unit components and I absolutely agree. But still, if you want to use assertions library to handle common cases where you are absolutely confident, you can use testify package. Writing tests with this package is very easy and fun.

In testing, there is a concept of Mock, Stubs, and Spies. You can read about them here. Since this article is already too long and Mock-Stub-Spy falls in advance testing techniques, we will cover them later in a separate article.


Testing in Go is easy but there are lots of hidden perks. If you need to learn more about the internals of the tests in Go, follow this documentation.

In upcoming tutorials, we will try to cover benchmarking in Go. Since the benchmarking tests have similar syntax as functional tests we have seen above, you can read about them from the official Go documentation.


RunGo

A place to find introductory Go Programming Language tutorials and learning resources. Like my other tutorials on Web Development, Run Go publication features important Go articles with deep dive into core of the language with examples and sample code.

Uday Hiwarale

Written by

IIT • Software Developer • India | Follow: github.com/thatisuday | Ask: thatisuday@gmail.com | World iza better place coz some still write on Medium for free :)

RunGo

RunGo

A place to find introductory Go Programming Language tutorials and learning resources. Like my other tutorials on Web Development, Run Go publication features important Go articles with deep dive into core of the language with examples and sample code.

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