Ayooluwa Isaiah I'm a software developer from Nigeria with a keen interest in web technologies, security, and performance. I'm currently working on my own products and teaching programming via my website freshman.tech.

# A deep dive into unit testing in Go In unit testing, developers test individual functions, methods, modules, and packages to verify their correctness. Unit testing helps to find and fix bugs early in the development cycle, and it prevents regressions when refactoring. A good unit test can also serve as a form of documentation for developers who are new to the project.

In this tutorial, we’ll cover how to write unit tests in Go using the built-in testing package and several external tools. By the end of this article, you’ll understand concepts like table driven tests, dependency injection, and code coverage.

Let’s get started!

## Writing your first test in Go

To understand testing in Go, we’ll write a basic program that computes the product of two integers. Then, we’ll write a test that verifies the correctness of its output.

First, create a directory on your file system and navigate into it. In the directory root, create a file called `integers.go` and add the following code:

```// integers.go
package main

import (
"fmt"
)

// Multiply returns the product of two integers
func Multiply(a, b int) int {
return a * b
}
```

Let’s write a test to verify that the `Multiply()` function works correctly. In the current directory, create a file called `integers_test.go` and add the following code to it:

```// integers_test.go
package main

import "testing"

func TestMultiply(t *testing.T) {
got := Multiply(2, 3)
want := 6

if want != got {
t.Errorf("Expected '%d', but got '%d'", want, got)
}
}
```

## Anatomy of a Go test

The convention for naming test files in Go is to end the file name with the `_test.go` suffix and place the file in the same directory as the code it tests. In the example above, the `Multiply` function is in `integers.go`, so its tests are placed in `integers_test.go`.

Note that Go does not ship test files in any binaries that it produces because they are not needed for the code to run. In Go, a test function must always use the following signature:

```func TestXxx(*testing.T)
```

A test’s name begins with the `Test` prefix, followed by the name of the function being tested, `Xxx`. It takes a single argument, which is a pointer of type `testing.T`. The type exports several methods for tasks like reporting errors, logging intermediate values, and specifying helper methods.

In our example in the previous section, the `got` variable inside the `TestMultiply()` function is assigned to the result of the `Multiply(2, 3)` function call. `want` is assigned to the expected result `6`.

The latter part of the test checks if the values of `want` and `got` are equal. If not, the `Errorf()` method is invoked, failing the test.

## Running Go tests

Now, let’s use the `go test` command to run our test in the terminal. As long as Go is installed, the `go test` command is already available on your machine.

The `go test` command compiles the sources, files, and tests found in the current directory, then runs the resulting test binary. When testing is done, a summary of the test, either `PASS` or `FAIL`, will be printed to the console, as seen in the code block below:

```\$ go test
PASS
ok      github.com/ayoisaiah/random 0.003s
```

When you use `go test` as above, caching is disabled, so the tests are executed every time.

You can also opt into package list mode by using `go test .`, which caches successful test results and avoids unnecessary reruns.

You can run tests in a specific package by passing the relative path to the package, for example, `go test ./package-name`. Additionally, you can use `go test ./...` to run the tests for all the packages in the codebase:

```\$ go test .
ok      github.com/ayoisaiah/random (cached)
```

If you append the `-v` flag to `go test`, the test will print out the names of all the executed test functions and the time spent for their execution. Additionally, the test displays the output of printing to the error log, for example, when you use `t.Log()` or `t.Logf()`:

```\$ go test -v
=== RUN   TestMultiply
--- PASS: TestMultiply (0.00s)
PASS
ok      github.com/ayoisaiah/random 0.002s
```

Let’s cause our test to fail by changing `want` to `7`. Run `go test` once again, and inspect its output:

```\$ go test -v
--- FAIL: TestMultiply (0.00s)
integers_test.go:10: Expected '7', but got '6'
FAIL
exit status 1
FAIL    github.com/ayoisaiah/random 0.003s
```

As you can see, the test failed, and the message passed to the `t.Errorf()` function is present in the failure message. If you return the `want` value to `6`, the test will pass once again.

## Table driven tests in Go

The test example from above contains only a single case. However, any reasonably comprehensive test would have multiple test cases, ensuring that each unit of code is sufficiently audited against a range of values.

In Go, we use table driven tests, which allow us to define all our tests cases in a slice, iterate over them, and perform comparisons to determine if the test case succeeded or failed:

```type testCase struct {
arg1 int
arg2 int
want int
}

func TestMultiply(t *testing.T) {
cases := []testCase{
{2, 3, 6},
{10, 5, 50},
{-8, -3, 24},
{0, 9, 0},
{-7, 6, -42},
}

for _, tc := range cases {
got := Multiply(tc.arg1, tc.arg2)
if tc.want != got {
t.Errorf("Expected '%d', but got '%d'", tc.want, got)
}
}
}
```

In the code snippet above, we use the `testCase` struct to define the inputs for each test case. The `arg1` and `arg2` properties represent the arguments to `Multiply`, while `want` is the expected result for the test case.

The `cases` slice is used to set up all the test cases for the `Multiply` function. Note that the property names are omitted for simplicity.

To test each case, we need to iterate over the `cases` slice, pass `arg1` and `arg2` from each case to `Multiply()`, then confirm if the return value is equal to what `want` specified. We can test as many cases as needed using this setup.

If you run the test again, it will pass successfully:

```\$ go test -v
=== RUN   TestMultiply
--- PASS: TestMultiply (0.00s)
PASS
ok      github.com/ayoisaiah/random     0.002s
```

## Signaling test failure

In the examples above, we’ve used the `t.Errorf()` method to fail tests. Using `t.Errorf()` is equivalent to invoking `t.Logf()`, which logs text to the console either on test failures or when the `-v` flag is provided, followed by `t.Fail()`, which marks the current function as failed without halting its execution.

Using `t.Errorf()` prevents a test failure when we halt the function, allowing us to gather more information to fix the problem. Additionally, in a table driven test, `t.Errorf()` allows us to fail a specific case without affecting the execution of other tests.

### More great articles from LogRocket:

If a test function cannot recover from a failure, you can stop it immediately by invoking `t.Fatal()` or `t.Fatalf()`. Either method marks the current function as failed, stopping its execution immediately. These methods are equivalent to calling `t.Log()` or `t.Logf()`, followed by `t.FailNow()`.

## Using subtests

Using a table driven test is effective, however, there is one major flaw – the inability to selectively run an individual test case without running all test cases.

One solution to this problem is to comment out all the test cases that are irrelevant in the moment and uncomment them again later on. However, doing so is tedious and error-prone. For this scenario, we’ll use a subtest!

In Go 1.7, we can split each test case into a unique test that is run in a separate goroutine by adding a `Run()` method to the `testing.T` type. The `Run()` method takes the name of the subtest as its first argument and the subtest function as the second. You can use the test name to identify and run the subtest individually.

To see it in action, let’s update our `TestMultiply` test, as shown below:

```func TestMultiply(t *testing.T) {
cases := []testCase{
{2, 3, 6},
{10, 5, 50},
{-8, -3, 24},
{0, 9, 0},
{-7, 6, -42},
}

for _, tc := range cases {
t.Run(fmt.Sprintf("%d*%d=%d", tc.arg1, tc.arg2, tc.want), func(t *testing.T) {
got := Multiply(tc.arg1, tc.arg2)
if tc.want != got {
t.Errorf("Expected '%d', but got '%d'", tc.want, got)
}
})
}
}
```

Now, when you run the tests with the `-v` flag, each individual test case will be reported in the output. Because we constructed the name of each test from the values in each test case, it’s easy to identify a specific test case that failed.

To name our test cases, we’ll add a `name` property to the `testCase` struct. It’s worth noting that the `TestMultiply` function does not finish running until all its subtests have exited:

```\$ go test -v
=== RUN   TestMultiply
=== RUN   TestMultiply/2*3=6
=== RUN   TestMultiply/10*5=50
=== RUN   TestMultiply/-8*-3=24
=== RUN   TestMultiply/0*9=0
=== RUN   TestMultiply/-7*6=-42
--- PASS: TestMultiply (0.00s)
--- PASS: TestMultiply/2*3=6 (0.00s)
--- PASS: TestMultiply/10*5=50 (0.00s)
--- PASS: TestMultiply/-8*-3=24 (0.00s)
--- PASS: TestMultiply/0*9=0 (0.00s)
--- PASS: TestMultiply/-7*6=-42 (0.00s)
PASS
ok      github.com/ayoisaiah/random 0.003s
```

## Measuring code coverage

Code coverage counts the lines of code that are successfully executed when your test suite is running, representing the percentage of your code covered by your test suite. For example, if you have a code coverage of 80 percent, it means that 20 percent of the codebase is lacking tests.

### Go’s built-in code coverage method

Go provides a built-in method for checking your code coverage. Since Go v1.2, developers can use the `-cover` option with `go test` to generate a code coverage report:

```\$ go test -cover
PASS
coverage: 100.0% of statements
ok      github.com/ayoisaiah/random 0.002s
```

We’ve managed to achieve 100 percent test coverage for our code, however, we’ve only tested a single function comprehensively. Let’s add a new function in the `integers.go` file without writing a test for it:

```// integers.go

// Add returns the summation of two integers
func Add(a, b int) int {
return a + b
}```

When we run the tests again with the `-cover` option, we’ll see coverage of just 50 percent:

```\$ go test -cover
PASS
coverage: 50.0% of statements
ok      github.com/ayoisaiah/random 0.002s
```

## Examining our codebase

Although we know what percentage of our codebase is covered, we don’t know what parts of our codebase aren’t covered. Let’s convert the coverage report to a file using the `--coverprofile` option so we can examine it more closely:

```\$ go test -coverprofile=coverage.out
PASS
coverage: 50.0% of statements
ok      github.com/ayoisaiah/random 0.002s
```

In the code block above, the tests run as before, and code coverage is printed to the console.
However, the test results are also saved to a new file called `coverage.out` in the current working directory. To study these results, let’s run the following command, which breaks the coverage report down by function:

```\$ go tool cover -func=coverage.out
github.com/ayoisaiah/random/integers.go:4:    Multiply    100.0%
total:                            (statements)    50.0%
```

The code block above shows that the `Multiply()` function is fully covered, while the `Add()` function has only 50 percent coverage overall.

### HTML coverage method

Another way to view the results is through an HTML representation. The code block below will open the default web browser automatically, showing the covered lines in green, uncovered lines in red, and uncounted statements in grey:

```\$ go tool cover -html=coverage.out
```

Using the HTML coverage method makes it easy to visualize what you haven’t covered yet. If the package being tested has multiple files, you can select each file from the input on the top right to see its coverage breakdown: Let’s get the code coverage back to 100 percent by adding a test for the `Add()` function, as shown below:

```func TestAdd(t *testing.T) {
cases := []test{
{1, 1, 2},
{7, 5, 12},
{-19, -3, -22},
{-1, 8, 7},
{-12, 0, -12},
}

for _, tc := range cases {
if tc.want != got {
t.Errorf("Expected '%d', but got '%d'", tc.want, got)
}
}
}
```

Running the tests again should display a code coverage of 100 percent:

```\$ go test -cover
PASS
coverage: 100.0% of statements
ok      github.com/ayoisaiah/random/integers    0.003s
```

## Running a specific test

Let’s say that you have many test files and functions, but you want to isolate only one or a few to execute. We can do so using the `-run` option. For example, if we want to run only the tests for the `Add` function, we’ll pass the test function name as an argument to `-run`:

```\$ go test -v -run=TestAdd
PASS
ok      github.com/ayoisaiah/random/integers    0.003s
```

As you can see from the output above, only the `TestAdd` method was executed. Note that the argument to `-run` is interpreted as a regular expression, so all the tests that match the provided regex will be run.

If you have a set of test functions that begin with the same prefix, like `TestAdd_NegativeNumbers` and `TestAdd_PositiveNumbers`, you can run them in isolation by passing the prefix, `TestAdd`, to `-run`.

Now, let’s assume that we only want to run `TestAdd` and `TestMultiply`, but we have other test functions. We can use a pipe character to separate their names in the argument to `-run`:

```\$ go test -v -run='TestAdd|TestMultiply'
=== RUN   TestMultiply
--- PASS: TestMultiply (0.00s)
PASS
ok      github.com/ayoisaiah/random/integers    0.002s
```

You can also run a specific subtest by passing its name to `-run`. For example, we can run any of the subtests in the `TestMultiply()` function, as shown below:

```\$ go test -v -run='TestMultiply/2*3=6'
=== RUN   TestMultiply
=== RUN   TestMultiply/2*3=6
--- PASS: TestMultiply (0.00s)
--- PASS: TestMultiply/2*3=6 (0.00s)
PASS
ok      github.com/ayoisaiah/random 0.003s
```

## Dependency injection

Let’s assume we have a function that prints some output to the console, as shown below:

```// printer.go
func Print(text string) {
fmt.Println(text)
}
```

The `Print()` function above outputs its string argument to the console. To test it, we have to capture its output and compare it to the expected value. However, because we have no control over the implementation of `fmt.Println()`, using this method won’t work in our case. Instead, we can refactor the `Print()` function, making it easier to capture its output.

First, let’s replace the call to `Println()` with a call to `Fprintln()`, which takes an `io.Writer` interface as its first argument, specifying where its output should be written. In our example below, this location is specified as `os.Stdout`. Now, we can match the behavior provided by `Println`:

```func Print(text string) {
fmt.Fprintln(os.Stdout, text)
}
```

For our function, it doesn’t matter where we print the text. Therefore, instead of hard-coding `os.Stdout`, we should accept an `io.Writer` interface and pass that to `fmt.Fprintln`:

```func Print(text string, w io.Writer) {
fmt.Fprintln(w, text)
}
```

Now, we can control where the output of the `Print()` function is written, making it easy to test our function. In the example test below, we’ll use a buffer of bytes to capture the output of `Print()`, then compare it to the expected result:

```// printer_test.go
func TestPrint(t *testing.T) {
var buf bytes.Buffer

text := "Hello, World!"

Print(text, &buf)

got := strings.TrimSpace(buf.String())

if got != text {
t.Errorf("Expected output to be: %s, but got: %s", text, got)
}
}
```

When utilizing `Print()` in your source code, you can easily inject a concrete type and write to the standard output:

```func main() {
Print("Hello, World!", os.Stdout)
}
```

Although the example above is quite trivial, it illustrates one method for moving from a specialized function to a general-purpose one, allowing for the injection of different dependencies.

## Conclusion

Writing unit tests ensures that each unit of code is working correctly, increasing the chance that your application as a whole will function as planned.

Having adequate unit tests also comes in handy when refactoring by helping to prevent regressions. The built-in testing package and the `go test` command provide you with considerable unit testing capabilities. You can learn more by referring to the official documentation.

Thanks for reading, and happy coding!

# Get setup with LogRocket's modern error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via NPM or script tag. `LogRocket.init()` must be called client-side, not server-side.
3. `\$ npm i --save logrocket // Code:import LogRocket from 'logrocket'; LogRocket.init('app/id');`
`Add to your HTML:<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script><script>window.LogRocket && window.LogRocket.init('app/id');</script>`
4. (Optional) Install plugins for deeper integrations with your stack:
• Redux middleware
• ngrx middleware
• Vuex plugin Ayooluwa Isaiah I'm a software developer from Nigeria with a keen interest in web technologies, security, and performance. I'm currently working on my own products and teaching programming via my website freshman.tech.