Like cells are the building units of our body, likewise, unit components make up software. Similarly, as the functioning of our body depends on the absolute efficiency and reliability of these cells, efficiency, and reliability of a piece of 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 the 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 that 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.
Here,
hello
function becomes a unit component of our program.
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.
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).
We can log additional information in verbose mode using t.Log
or t.Logf
method as shown below.
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.
Even though this is not a good approach to create two test functions to test the same functionality, we are doing it here just for an example.
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).
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.
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.
When you have test cases (_test.go
files) in your executable(main) package, you can’t simply execute go run *.go
to run the project. *.go
part also matches the test files (_test.go
files) and go run
command can’t run them and returns go run: cannot run *_test.go files (hello_test.go)
error.
But you don’t necessarily need to use go run *.go
command. You can use go run .
command run the current package (or module) or go run ./<package>
command to any package in a module. Also, you can use go install
and go build
command from within a package (or module).
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.
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.
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.
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.
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.
You can use
go test
command in the module to run test suit, if your module contains package source code. Which meansgo test
command is valid if the end user imports your module like a package.
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](/golang/conditional-statements-and-loops)
loop). The best type to hold data of different types is a struct (read more about structs).
15
In the above example, we have defined InputDataItem
struct type which holds input numbers to Add
function (inputsfield), 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.
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](https://golang.org/pkg/reflect/#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 ./...
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.
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 _tes``t
. 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.
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.go
file, 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.