Write Your First Test
Learn the fundamentals for writing tests with Go.
We'll cover the following
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:
package helloworldfunc 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) } }
The example above only involves two files:
- The
main.go
file holds the actual code. In our case, it’s only made by theSum
function. - The
main_test.go
file contains our test code that evaluates the correct behavior of theSum
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 rungo 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
, whereXxx
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) } }
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.