Write Your First Test

Learn the fundamentals for writing tests with Go.

Introduction

Let’s start with a basic example of tests in Go. Below, we can see a simple function that we’d like to test:

Press + to interact
package helloworld
func Sum(a, b int) int {
return a + b
}

This function is called Sum, and it accepts the parameters a and b both of type int. It sums them up and then returns their sum. Lastly, it belongs to the helloworld package. Let’s see how to define a basic test for it:

package helloworld

import "testing"

func TestSum(t *testing.T) {
	got := Sum(1, 2)
	if got != 3 {
		t.Errorf("expected to get 3 but got %d", got)
	}
}
A basic test in Go

The example above only involves two files:

  • The main.go file holds the actual code. In our case, it’s only made by the Sum function.
  • The main_test.go file contains our test code that evaluates the correct behavior of the Sum function.

To execute tests in a package, we need to issue the go test command, which displays the outcome of our tests: PASS or FAIL. If we would like to have a more lengthy result that also involves the list of passed tests, we have to use go test -v.

Note: With the go test command, we only run the tests defined in the current package. In other words, only tests defined within the folder where we’re in our terminal will be performed. To run all the module’s tests, we should run go test ./...

Now, let’s focus on the test code to understand it better. First, this is called white-box testing because the test code resides in the same package as the production code (helloworld in this case). With white-box testing, we are able to access everything defined in the production code (including unexported things) within our test functions.

Note: The opposite of white-box testing is black-box testing. To achieve black-box testing, we need to write our test code in a different package than the production code. In our example, we could have written the code in a package called helloworld_test just to follow the Go naming convention. In this way, we wouldn’t have access to the internals, just the exported values and functions.

In Go, we don’t have to rely on an external testing framework because everything we need for our tests is already built into the language. We just have to import the testing package, and we’re ready to write our tests.

Naming conventions

Another essential aspect when it comes to tests is naming. Below, we can find a list of good naming rules that we need to strive to:

  • The name of the test function must have the format TestXxx, where Xxx is the capitalized name of the function we’re going to test.
  • Conventionally, SUT means system under test, which is the component we’re testing. Often, this is a struct with some methods that we can run on its instances.
  • Usually, got refers to the outcome provided by the system under test. This result should be asserted to check if it complies to the expectations or not.

Failing tests

Test execution can have two results: it can succeed or it can fail. If we execute the test function code without raising any errors, the function call is successful. However, we can also mark the test as failed if the expectations are not met. To mark a test with a failing status, we need to call Errorf on the t struct. Errorf also accepts a format template, so we can provide it with data to print a more meaningful message to the user. Let’s introduce a little bug in the previous code to see a failing test:

package helloworld

import "testing"

func TestSum(t *testing.T) {
	got := Sum(1, 2)
	if got != 3 {
		t.Errorf("expected to get 3 but got %d", got)
	}
}
Example of a failing test

In the previous code sample, we intentionally introduced a bug in the Sum function. Instead of adding, we subtracted. Thanks to this little bug, we can see the output provided by a failing test. When we run our test, it returns the result main_test.go:8 expected to get 3 but got -1. This message is self-explanatory, so we can easily spot where the bug resides and get rid of it.