Testing in Go, as in any language, is a subject worthy of its own book. Unfortunately, covering all topics, such as benchmarking, example tests, and design patterns, is outside the scope of this book. In this chapter, we cover the fundamentals of testing in Go, including writing, running, and debugging tests. We also cover table driven tests, test helpers, and code coverage.
Go ships with a powerful testing framework, and as such, there is a strong emphasis on testing in Go. Tests in Go are written in the Go language, so there is no need to learn another syntax. Because the language was designed with testing in mind, it tends to be easier to create and understand tests in Go than in some other languages.
As with any statically typed language, the compiler catches a lot of bugs for you. However, it cannot ensure your business logic is sound or bug free. In Go, there are several tools to ensure your code is bug free (such as tests), as well as tools to show you what part of your code has been tested and which has been not (code coverage).
We introduce some of the basic testing features now, and as we continue to introduce new topics later, we show the corresponding test features and approaches as they pertain to those topics and features.
Up until now, there has not been much convention to the naming or placement of files or functions in our Go code. However, with testing, naming of files and functions play an incredibly important part of how tests work.
When creating the code for a test, that code has to be placed in a test
file. Go identifies a file as being a test file with the test suffix. If a file has a _test.go
suffix, Go knows to process that as a test
.
A nice side effect of this naming convention is that all test files appear in the file list right next to the file they are usually testing, as in Listing 7.1.
This naming convention is a required naming convention. Even if you write test code in another file, if it does not have the _test.go
suffix, the Go framework does not run the code as a test.
Listing 7.1 Test Files End in _test.go
and Live Next to the File They’re Testing
Aside from putting each test in a _test.go
file, you also need to name your test functions properly. Without the proper naming and signature, Go does not run the function as a test.
Each test function must have the signature Test<Name>(*testing.T)
.
In Listing 7.2, the function starts with the uppercase word Test
. You can have other functions in your test files, but only the functions that start with the Test
keyword run as a test. Additionally, each test must accept the (t *testing.T)
argument.
Listing 7.2 A Simple Go Test
testing.T
TypeThe testing.T
1 type, Listing 7.3, is used to validate and control the flow of a test. It has the following methods available to use to control the flow of a test. We cover most of the testing.T
type, and its methods, in this chapter.
1. https://pkg.go.dev/testing#T
Listing 7.3 The testing.T
Type
Two of the most common calls to make while testing are testing.T.Error
,2 Listing 7.4, and testing.T.Fatal
,3 Listing 7.5. Both report a test failure, but testing.T.Error
continues with execution of that test, whereas testing.T.Fatal
ends that test.
2. https://pkg.go.dev/testing#T.Error
3. https://pkg.go.dev/testing#T.Fatal
Listing 7.4 The testing.T.Error
Method
Listing 7.5 The testing.T.Fatal
Method
t.Error
In Listing 7.6, the GetAlphabet
function returns a slice of letters for each country’s alphabet. For example, if given the key US
, the GetAlphabet
function returns a slice of 26 characters, A-Z
, and a nil
to represent no error. If the requested key is not present in the map, then a nil
is returned in place of the map, and the fmt.Errorf
function is used to return an error.
Listing 7.6 A Function That May Return an Error
The test in Listing 7.7 for the GetAlphabet
function marks test errors with the testing.T.Errorf
4 function. This tells Go that the test has failed; however, the test continues to run.
4. https://pkg.go.dev/testing#T.Errorf
Listing 7.7 Using testing.T.Errorf
to Indicate a Test Failure
In Listing 7.7, the key CA
does not exist. The first error, could not find alphabet CA
, is logged, and the test continues to run. The next check is to confirm that the thirteenth letter of the alphabet is the letter M
. Because the alphabet was not found, the alpha
slice is empty, so when the test tries to retrieve index 12
, the test panics.
t.Fatal
(Recommended)Listing 7.8 uses testing.T.Fatalf
5 instead of testing.T.Errorf
. This time, the test fails immediately at the first error, could not find alphabet CA
. Since the test stops at that first error, the alpha[12]
call is never executed, and the test doesn’t panic.
5. https://pkg.go.dev/testing#T.Fatalf
Listing 7.8 Using testing.T.Fatalf
to Indicate a Test Failure
In summary, when trying to determine when to use testing.T.Fatal
or testing.T.Error
, it usually comes down to this: If the test continues but would result in more failures due to the current condition failing, you should prevent the test from continuing by using testing.T.Fatal
. If the test could continue after failing a condition but still report other errors that are valid, then use testing.T.Error
.
When you decide that a test has failed, you need to make sure to craft a well-written failure message that informs the person running the test what the failure was and why. This failure message should also be resilient to change in the test later.
Consider the test in Listing 7.9. The test is designed to fail if the result of the AddTen(int)
method does not return 11
.
Listing 7.9 A Test That Fails
While the error message in Listing 7.9 is very well-written, we have hard coded the expected value in two places. If we update one and forget to update the other, the resulting output would be very confusing. This is what we mean by all test messages should be “resilient to change” for future changes.
By using variables to hold data, Listing 7.10, like the expected value, we can easily update the code in one place and know that our error messages will be updated accordingly.
Listing 7.10 A Test That Fails but with a Better Error Message
Now if the future expected value changes, only one variable needs to be changed, which reduces the chance that an invalid error message would be rendered.
One of the most important parts of writing a solid application that is robust and maintainable is to ensure that the code is well tested. This is especially important when you are working with large code bases. In this section, we cover the basics of code coverage and how to use it to help you write better tests.
To write the best tests, you need to know which branches of the code are covered by tests. You can do this by generating code coverage reports.
Let’s start by looking at code coverage for a package. When we run go test
, Listing 7.11, the tests pass, but there is no code coverage information.
Listing 7.11 Running Tests with No Coverage Information
In Listing 7.12, we run go test
with the -cover
flag. We get an overall code coverage printed for the package or packages being tested.
Listing 7.12 Running Tests with Coverage Information
The -cover
flag can be run both locally and in CI to help get a quick view into how well your packages are tested.
While the -cover
flag gives you a quick view of how well your tests are covering your code, you can also generate a detailed coverage profile. This report gives you a breakdown of which lines of code are covered by tests and which lines are not.
In Listing 7.13, we pass the -coverprofile
flag with a value of coverage.out
to go test
to generate a coverage profile. The tests run, and the coverage.out
file is generated with the coverage information.
Listing 7.13 Generating a Coverage Profile
The file, coverage.out
, as shown in Listing 7.14, generated by Listing 7.13, contains a list of all the lines of code that were covered by the tests.
Listing 7.14 Contents of the coverage.out
File
go tool cover
CommandA coverage file, like the one in Listing 7.14, is not meant to be viewed directly. Instead, it’s meant to be used with the cover.cmd
command, Listing 7.15.
Listing 7.15 The go tool cover
Command
The go tool cover
command can be used to transform the coverage profile generated by using the -coverprofile
flag with go test
. For example, in Listing 7.16, we use the go tool cover
command to view the coverage profile generated by Listing 7.13 on a per-function basis.
Listing 7.16 Viewing the Coverage Profile on a Per-Function Basis
Most often, you want to generate an HTML coverage report. This report gives you a nice visual breakdown of which lines of code are covered by tests and which lines are not. You can use the -html
flag with a value of coverage.out
, Listing 7.17, to generate an HTML coverage report from the coverage profile generated in Listing 7.13. The result opens in your default browser.
In Listing 7.17, we are calling the go tool cover
command with the -html
flag and the name of the coverage file, coverage.out.
When run locally, this opens the default OS browser and displays an HTML output of the code coverage, as shown in Figure 7.1.
Listing 7.17 Generating an HTML Coverage Report
You can use the -o
flag, as shown in Listing 7.18, to specify an output file instead of opening the report in your default browser.
Listing 7.18 Generating an HTML Coverage Report with an Output File
Figure 7.1 shows a copy of code coverage in a browser. The report highlights lines that are covered by the tests in green, and lines that are not covered in red. Gray lines are not tracked by coverage—for example, type definition and function declarations or comments.
A lot of editors, and their Go plugins, support code coverage. For example, the Go extension6 for Visual Studio Code supports displaying code coverage directly in the editor, as shown in Figure 7.2.
The code coverage demonstrated in Listing 7.19 shows that the All
method has several code paths that are currently not covered by tests. Table driven tests can help us fix this.
Listing 7.19 Viewing the Coverage Profile on a Per-Function Basis
You can use table driven tests to cover a lot of ground quickly while reusing common setup and comparison code. Table driven testing is not unique to Go; however, it is very popular in the Go community.
Listing 7.20 is an example of the way a table driven test is structured. We use a slice of anonymous structs. The anonymous struct defines any fields needed for each test case; then we can fill that slice with the test cases we want to test. Finally, we loop through the slice of test cases and perform the tests.
Listing 7.20 Anatomy of a Table Driven Test
Given the All
method in Listing 7.21, we can use table driven tests to cover all of the error paths in the code.
Listing 7.21 The All
Method
In the test defined in Listing 7.22, we first set up our common test data. For example, we can define different implementations of the Store
type that have various differences, such as whether their internal data
field is initialized.
After that, we create a new slice of anonymous structs that hold the test cases we want to test. In this case, we need to be able to set the Store
and the error we expect to see.
Finally, we loop through the slice of test cases and perform the tests.
Listing 7.22 Table Driven Tests with No Subtests
As you can see from the test output, Listing 7.23, our tests pass, but they are all in one test.
Listing 7.23 Output of the Table Driven Test
If one of the test cases fails, Listing 7.24, we don’t know which one it is. We also don’t have the ability to run just that one test. Using subtests solves both of these problems.
Listing 7.24 Uncertainty about Which Test Case Failed
One of the problems with our tests as they are currently written is that they are not very descriptive, and if one of those test cases fails, we don’t know which one, as we saw in Listing 7.24. This is where subtests come in. The testing.T.Run
7 method, Listing 7.25, allows us to create a whole new test for each test case we define. In Go, the name of the test is derived from the name of the test function. For example, if we have a test function called TestSomething
, the name of the test is TestSomething
. With subtests, we can create our own names for each test case to make them as useful and descriptive as we need.
7. https://pkg.go.dev/testing#T.Run
Listing 7.25 The testing.T.Run
Method
Using subtests is not much different than what we have been doing with table driven tests so far (refer to Listing 7.20). The biggest difference is in the for
loop. In Listing 7.26, instead of making our assertions directly, we are, instead, calling the testing.T.Run
method to create a new subtest. We give the testing.T.Run
method the name of our test case and the function we want to run as a subtest.
Listing 7.26 Anatomy of a Subtest
In Listing 7.27, we have added a call to the testing.T.Run
method to create a subtest for each test case. When the tests are run, each test case is displayed as its own test. Should one of the subtests fail, it is easy to identify which one it is.
Listing 7.27 Subtests Displayed as Their Own Tests
Understanding the different options to run tests can greatly reduce the feedback cycle during development. In this section, we cover running specific tests, verbose output, failing fast, parallel options, and more.
You can run all of the tests in the current package (folder) using the go test
command without specifying any test paths, as shown in Listing 7.28.
Listing 7.28 Running All the Tests in the Current Package
Often, Go projects consist of multiple packages. To run all of these packages, you can use the ./...
, Listing 7.29, identifier to tell Go to recurse through all subpackages as well as the current package.
Listing 7.29 Running All the Tests in the Current Package and Subpackages
Optionally, as in Listing 7.30, you can specify the path of one or more packages to test.
Listing 7.30 Running All the Tests in the models
Package
It can be useful to output “verbose” information when running tests, such as when you’re trying to debug a test. For example, as in Listing 7.31, you can see each test and subtest broken out individually. This allows you to see which tests were run and the status of that individual test. The -v
flag turns on this verbose output.
Listing 7.31 Verbose Test Output
When using the -v
flag, anything printed to the standard output is also displayed, regardless of whether the test was successful. This includes calls to fmt.Print
and similar functions. You can use the testing.T.Log
8 and testing.T.Logf
9 methods to log information during tests. By default, any testing.T.Log
and testing.T.Logf
statements show up in test output only if the test fails. In Listing 7.32, you can see that when we run our test suite without the -v
flag, there is no extra output.
8. https://pkg.go.dev/testing#T.Log
9. https://pkg.go.dev/testing#T.Logf
Listing 7.32 Logging in Tests
When tests are run with the -v
flag, Listing 7.33, the output of testing.T.Log
and testing.T.Logf
statements is shown.
Listing 7.33 Enabling Logging in Tests with the -v
Flag
Sometimes you have long running tests, or integration tests, that you might not want to run as part of your local development cycle. The testing.Short
10 function shown in Listing 7.34 can be used to mark a test as being “short.”
10. https://pkg.go.dev/testing#Short
Listing 7.34 The testing.Short
Function
You can pass the -short
argument to the test runner and check for that in your tests with the testing.Short
function, as in Listing 7.35. This is useful also if your test has outside dependencies such as a database that you might not have available for local testing.
Listing 7.35 Using the -short Flag and testing.Short
to Run Specific Tests
By default, each package is run in parallel to make testing faster. This can be changed by setting the -parallel
flag to 1
, as in Listing 7.36. This is usually done to prevent packages that use transactions to a database or would otherwise cause contention between package tests.
Listing 7.36 Changing the Parallelism of the Test Runner
Although packages are run in different threads during testing, individual tests within those packages are not. They are run one test at a time. However, you can change this.
By using testing.t.Parallel
,11 Listing 7.37, you signal that this test is to be run in parallel with (and only with) other parallel tests.
11. https://pkg.go.dev/testing#t.Parallel
Listing 7.37 The testing.T.Parallel
Method
In Listing 7.38, the first thing we do inside of the Test_Example
test function is to call the t.Parallel
method to tell Go that this test is safe to be run concurrently with other tests.
Listing 7.38 Using testing.t.Parallel
to Mark a Test as Parallel-Friendly
The -run
flag allows for the passing of a regular expression to match the names of specific tests to run. For example, in Listing 7.39, we are passing the -run
flag to the test runner to run only tests that contain the string History
.
Listing 7.39 Running Only Tests That Have History
in Their Name
In Listing 7.40, we are passing the -run
flag to the test runner to run only tests that contain the string Address
.
Listing 7.40 Subtests Are Also Run with the -run
Flag
Eventually you will accidentally write an erroneous infinite for
loop or other piece of code that will cause your tests to run forever. By using the -timeout
flag, you can set a timeout for your tests to prevent these scenarios. In Listing 7.41, the tests stop and fail after fifty milliseconds.
Listing 7.41 Setting a Timeout for Tests with the -timeout
Flag
You can use the -failfast
flag to stop your tests at the first test failure. This is specifically very useful when you’re doing large refactors and you want tests to stop as soon as one test fails.
In Listing 7.42, we are running the tests without the -failfast
flag. As we see from the output, all the tests are run before the failures are reported. In Listing 7.43, we are running the tests with the -failfast
flag, and as you see from the output, once the first test fails, the rest of the tests are not run.
Listing 7.42 Setting the -failfast
Flag
Listing 7.43 Setting the -failfast
Flag
Go automatically caches any passing tests to make subsequent test runs faster. Go breaks this cache when either the test or the code it is testing changes. This means you almost never have to deal with test caches. Occasionally there are times when you might want to disable this behavior.
One reason you may want to disable caching is if you have tests that are integration tests and test packages or systems external to the package the tests are written in. Because it’s likely the package didn’t experience changes but the outside dependencies did, you would not want to cache your tests in this scenario.
To ensure your tests don’t use any cached runs, use the -count
flag, as shown in Listing 7.44, and force the tests to run at least once.
Listing 7.44 Disabling the Test Cache with the -count
Flag
Just like when we’re writing “real” code, our tests sometimes need helper functions. These helper functions are called “test helpers.” Test helpers might set up, tear down, or provision resources for the test. They can be used to write assertions or to mock out external dependencies.
Test helper functions in Go are just like any other function. The difference is that they are only defined in your tests. While not required, it is recommended that you take the testing.TB
interface, Listing 7.45, as the first argument to your test helper function. This interface is the common set of functions to both the testing.T
12 and testing.B
,13 used for benchmarking, types.
12. https://pkg.go.dev/testing#T
13. https://pkg.go.dev/testing#B
Listing 7.45 The testing.TB
14 Interface
14. https://pkg.go.dev/testing#TB
Let’s define some test helpers to help clean up tests in Listing 7.46. We create helpers to create the different Store
implementations we need for the test.
Listing 7.46 The Test Function to Refactor with Helpers
First, in Listing 7.47, we create two helper functions. The first, noData(testing.TB)
, returns a &Store
that does not have any data. The second, withData(testing.TB)
, returns a &Store
that has its data
field properly initialized.
Listing 7.47 Two Helper Functions for Initializing Store
Values
In Listing 7.48, we declare the withUsers
helper function needed to clean up the tests. At the moment, however, we are unable to implement the withUsers
test helper, so we can call the testing.TB.Fatal
15 method on the passed-in testing.TB
to let Go know that we haven’t implemented this helper yet.
15. https://pkg.go.dev/testing#TB.Fatal
Listing 7.48 An Unimplemented Helper Function
Finally, in Listing 7.49, we can update our tests to use the new test helpers, passing in the testing.T
argument from the test to the helper function.
Listing 7.49 The Test Function with the New Helpers
As shown in Listing 7.50, because we have yet to implement the withUsers
helper, our tests will fail.
Listing 7.50 The Stack Trace Prints the Line in the Helper That Failed
When the test fails, it reported the test failure as inside the withUsers
helper, Listing 7.48, and not within the test itself. This can make it difficult to debug the test failure.
To get Go to report the correct line number, inside of the test and not the helper, we need to tell Go that the withData
function is a test helper. To do that, we must use the testing.TB.Helper
16 method, Listing 7.51, inside of our test helpers.
16. https://pkg.go.dev/testing#TB.Helper
Listing 7.51 The testing.TB.Helper
Method
Now, when the test fails, in Listing 7.52, the line number reported is the line number inside the test that failed, not the line number inside the helper.
Listing 7.52 The Stack Track Now Points to the Text Line That Failed
It is very common that a test helper, or even a test itself, needs to clean up some resources when done. To do that, you need to use the testing.TB.Cleanup
17 method, Listing 7.53. With testing.TB.Cleanup
, we can pass in a function that is called automatically when the test is done.
17. https://pkg.go.dev/testing#TB.Cleanup
Listing 7.53 The testing.TB.Cleanup
Method
In Listing 7.54, we implement the withUsers
helper function. In it, we use the testing.TB.Cleanup
method to clean up the users we created.
Listing 7.54 Using testing.T.Cleanup
to Clean Up Helper Resources
Cleanup
versus defer
Although the testing.TB.Cleanup
method might seem like a fancy defer
statement, it is actually a different concept. When a function is deferred, that function is called when its parent function returns. In the case of testing.TB.Cleanup
, the testing.TB.Cleanup
function is called when the test is done.
In this chapter, we covered the basics of testing in Go. We covered how to write tests, how to run tests, how to write test helpers, and how to write table driven tests. We showed you how to generate code coverage reports and how to run tests in parallel. We also covered many of the important test flags, such as -run
, -v
, and -timeout
. Finally, we explained how to write useful test helpers and how to properly clean up after them.