Testing in Go: First Principles
Tags: go, testing
Published: 2019-06-17 21:53:05

---
title: Testing in Go: First Principles
published: true
tags: [go, testing]
canonical_url: https://ieftimov.com/post/testing-in-go-first-principles/
series: "Testing in Go"
---

If you have any programming experience, whether that’s as a student or a professional, there’s a good chance you have heard about testing. It’s an omnipresent topic, be it on conferences, books or articles. (See what I did there?)

Also, it seems like a topic that everyone agrees on - yes, testing is good and we should do it. There are many reasons why folks consider testing good for you code. But, before we go down the rabbit hole and discuss the pros and cons of testing, let’s learn how we can test our Go code.

Of course, through actual examples that you can follow along.

## What is testing?

Now, before we go on, I’ll take you on a short trip down memory lane. At this moment of my carreer I have probably written thousands (if not tens of thousands) of test cases. And I have failed quite a bit at testing, especially as a novice. So if you are new to test or just haven’t gotten it under your belt yet – worry not, I got you covered.

Before you start with testing there’s one idea I would like you to know: just like your programs are consisted of code, also your tests are code. And really, that’s something you have to remember. It’s simple - test are code.

What this testing code does is it invokes the code that powers your programs and checks if what it returns is what is expected. Quite simple, innit?

It all revolves around setting expectations and then making your program meet these expectations. Usually languages provide you with packages or libraries to test your code, with each language or library having its own conventions on what testing looks like. But really, it’s just that - meeting expectations.

That’s testing in a nutshell. Let’s move on.

## What is a test?

So, what is a test then?

It’s quite simple - repeatable steps by which one can verify if something is working as it is supposed to.

What is a test in Go terms?

Quite similarly, it’s a piece of code that you can run many times and it will check if your code is working as intended.

That’s it. Shall we write one?

## Testing our Go code

To write a test, we first have to have a program to test. Let’s implement a simple function that will take a slice of `int` and return its biggest number:

```go
func Max(numbers []int) int {
	var max int

	for _, number := range numbers {
		if number > max {
			max = number
		}
	}

	return max
}
```

Pretty simple. We take a slice of `int`s and return the largest. So, how can we test that our code works as expected?

Let’s write a function that will take two arguments: a slice of `int`s and the maximum of the `int`s in that slice. We will call it `TestMax`:

```go
func TestMax(numbers []int, expected int) string {
	output := "Pass"
	actual := Max(numbers)
	if actual != expected {
		output = fmt.Sprintf("Expected %v, but instead got %v!", expected, actual)
	}
	return output
}
```

The `TestMax` function will check if the result of the `Max` function call matches the `expected` result. If it does, it will simply return `"Pass"`, otherwise it will return an informative string with what it was expecting and what it got.

Let’s use it in our `main` function:

```go
func main() {
	fmt.Println(TestMax([]int{1, 2, 3, 4}, 4))
}
```

The `main` function will invoke the `TestMax` once here, with a slice that contains `1,2,3,4` as argument and `4` as the expected maximum.

You might already be thinking that the example will pass. Let’s run it:

```
$ go run max.go
Pass
```

As expected - the example passed. Let’s add two more:

```go
func main() {
	fmt.Println(TestMax([]int{1, 2, 3, 4}, 4))
	fmt.Println(TestMax([]int{4, 2, 1, 4}, 3))
	fmt.Println(TestMax([]int{0, 0, 0, 0}, 1))
}
```

Here we add two more examples, with two different pairs of arguments:

1. A slice containing `4,2,1,4` and `3`, the expected maximum
2. A slice containing four zeroes, and `1` as the expected maximum

If we would run it, both of these examples would fail. The reason for the failures would be that none of the two slices of `int`s that we added do not match the expected maximum that we pass as a second argument to both of the calls. In the second example the maximum is `4`, while we expect a `3`. In the third example, the maximum is `0` while we expect `1`.

Testing our `Max` function can be done with just one function (`TestMax`). As long as we can / an input for the function and an expected output we can test our functions.

What is important to understand here is that testing can be very simple. Here, we are able to check if our function is working as expected without any fancy frameworks and libraries - just plain Go code. If you have any experience with testing you already know that this approach does not scale too far, but it’s very good to understand the idea that tests are just code.

Using this approach, we could technically even write our own testing framework/library. The good thing is that Go already has a testing package included in its standard library, so we can avoid that.


## Testing with Go’s `testing` package

Golang’s [`testing`](https://golang.org/pkg/testing/) package provides support for automated testing of Go packages. It exposes a set of useful functions that we can use to get couple of benefits: a better looking code, standardised approach to testing and nicer looking output. Also, we eliminate the need to create our own reporting of failed/passed tests.

Let’s see how we could test our `Max` function using the `testing` package.

First, we need to create a `max_test.go` file, which will be the test counterpart to our `max.go` (where our `Max` function is defined). Most importantly, both of the files have to be part of the same package (in our example `main`):

```go
// max_test.go
package main

import "testing"

func TestMax(t *testing.T) {
	actual := Max([]int{1, 2, 3, 4})
	if actual != 4 {
		t.Errorf("Expected %d, got %d", 4, actual)
	}
}
```

The `testing` package that is imported allows for benchmarking and testing. In our test we use the `testing.T` type, which when passed to Test functions manages the test state and formats the test logs.

Within the `TestMax` function, we get the result of the `Max` function and we assign it to `actual`. Then, we compare `actual` to the expected result (`4`). If the comparison fails, then we make the test fail by using the `t.Errorf` function and we supply the error message.

## What happens when we run `go test`

Golang’s `testing` package also comes with its buddy – the `go test` command. This command automates testing the packages named by the import paths. `go
test` recompiles each package along with any files with names matching the file pattern `*_test.go`.

To run this file, we have to use the `go test` command:

```
› go test
PASS
ok github.com/fteem/testing_in_go	0.007s
```

As you can see, we did not need to tell Golang which tests to run - it figured this out on its own. This is because `go test` is smartly done, with two different running modes.

The first mode, is called local directory mode. This mode is active whe the command invoked with no arguments. In local directory mode, `go test` will compile the package sources and the tests found in the current directory and then will run the resulting test binary.

After the package test finishes, `go test` prints a summary line showing the test status (`ok` or `FAIL`), package name, and elapsed time. This is what we actually see in our output - our test have passed. Looking at the output above we can also see that they passed in `0.007s`. Pretty fast!

The second mode is called package list mode. This mode is activated when the command is invoked with explicit arguments. In this mode, `go test` will compile and test each of the packages listed as arguments. If a package test passes, `go test` prints only the final ‘ok’ summary line. If a package test fails, go test prints the full test output.

For now, we can stick with using the first mode. We will see when and how to use the second mode in one of the next articles.

## Dealing with test failures

Now that we have a passing example, let’s see what tests failures look like. Let’s add another example, which we will purposely fail:

```go
func TestMaxInvalid(t *testing.T) {
	actual := Max([]int{1, 2, 3, 4})
	if actual != 5 {
		t.Errorf("Expected %d, got %d", 5, actual)
	}
}
```

The `TestMaxInvalid` is very similar to the test function we had before, with having the wrong expectations being the only difference. Simply put - we know that `Max` will return `4` here, but we are expecting a `5`.

While we are here, let’s add one more example where we would pass an empty slice as an argument to `Max` and expect `-1` as a result:

```go
func TestMaxEmpty(t *testing.T) {
	actual := Max([]int{})
	if actual != -1 {
		t.Errorf("Expected %v, got %d", -1, actual)
	}
}
```

Let’s run `go test` again and see the output:

```
$ go test
--- FAIL: TestMaxInvalid (0.00s)
    max_test.go Expected 5, got 4
--- FAIL: TestMaxEmpty (0.00s)
    max_test.go Expected -1, got 0
FAIL
exit status 1
FAIL	github.com/fteem/testing_in_go	0.009s
```

The two new tests failed unsurprisingly. If we inspect the output here we will notice that there are two lines per failed tests. Both of these lines, start with `--- FAIL:` and have the test function name after. At the end of the line there’s also the time it took for the test function to run.

In the second lines, we see the test file name with the line number of where the failure occurred. More specifically, this is where in both of our test files we invoke `t.Errorf`.

Let’s make our tests pass. First, we need to fix the expectation in the `TestMaxInvalid` test function:
 
```go
func TestMaxInvalid(t *testing.T) {
	actual := Max([]int{1, 2, 3, 4})
	if actual != 4 {
		t.Errorf("Expected %d, got %d", 4, actual)
	}
}
```

Now, when we run it we should see one less failure:

```
› go test
--- FAIL: TestMaxEmpty (0.00s)
    max_test.go Expected -1, got 0
FAIL
exit status 1
FAIL	github.com/fteem/testing_in_go	0.006s
```

Good. We could technically remove the `TestMaxInvalid` as it is the same as the `TestMax` function. To make the other test pass, we need to return `-1` when the slice received as argument in `Max` is empty:

```go
package main

func Max(numbers []int) int {
	if len(numbers) == 0 {
		return -1
	}

	var max int

	for _, number := range numbers {
		if number > max {
			max = number
		}
	}

	return max
}
```

The `len` function will check the length of the `numbers` slice. If it’s `0`, it will return `-1`. Let’s run the tests again:

```
› go test
PASS
ok github.com/fteem/testing_in_go	0.006s
```

Back to passing tests. With our new change the `Max` function will return `-1` when the slice in arguments is empty.

## In closing

What we talked about in this article is about what tests are in fact. We understood that testing is useful and that test are just code - nothing more. We saw how we could test our own code without any libraries or frameworks, with just simple Golang code.

Then, we went on to explore Golang’s `testing` package. We saw how an actual test function looks like. We talked about function definitions, the `testing.T` argument that we have to pass in and how to fail a test. Then we added some more tests for our `Max` function and made its tests pass.

As you can see, testing in a nutshell is a very simple but powerful technique. With a little bit of simple code we can assure that our code functions in an expected matter, that we can control. And with any new functionality added to our code, we can easily throw in another test to make sure it is covered.

Obviously, there is much more to testing that we will explore in other articles, but now that we are confident with these basic ideas and approaches we can build our knowledge on top of.

Before we stop here, please let me know in the comments what you like and dislike about testing your code? Also, what topics in testing you find confusing or challenging?