Intro to automated testing in Go
Tags: go, testing, codenewbie, beginners
Published: 2021-03-09 18:47:02

---
title: Intro to automated testing in Go
published: true
description: Testing with Go's standard library for newcomers to Go
tags: #go, #testing, #codenewbie, #beginners
series: Intro to automated testing in Go
---

> This blog post is adapted from an intro to testing talk I recently did at [OrlanGo](https://meetup.com/orlango)'s virtual meetup. You can find the slides in [this GitHub repository](https://github.com/andyhaskell/orlango-testing-talk).

There are a lot of features to the Go programming language that I really like, and one of my favorites is that writing automated tests in Go is really encouraged.

The moment you install Go, right out of the box without having to install **anything else**, you have access to a Go test runner for automated testing. I've personally written Go contributing to open source, working at a startup, and today working at Salesforce, and the convenience of testing in Go has always helped me build. So in this tutorial, I would like to show you:

* 💡 Why write automated Go tests?
* 🐹 How you'll use the `testing` package in Go
* 📋 What testing your Go code will look like as part of your workflow

If you're coming to Go from automated testing in Jest or RSpec or you're new to automated testing as a whole, this tutorial is for you!

## 💭 Why write tests?

When we're building an app, we're already maintaining a lot of code to begin with, so we should have a good reason if we're going to be writing some more. So here are some benefits to giving your code test coverage:

First, writing tests has you **write out each category of scenario your code is intended to work with**. This way, when you are writing the main code for your software, you have those scenarios in mind. In fact, some engineers even write out the test coverage for the code they're writing and then write the code. That technique is called **test-driven development** and it's not everyone's thing, but it's worth a try!

Another benefit is that since big software projects are made of smaller pieces of code, writing tests **helps you make those smaller pieces of code serve as a source of truth**. That way when you're building with those units of code, you have confidence that they work as expected.

Also, if you have automated testing, something machines are great at is **repeating a test in exactly the same way**. That's not to say manual testing doesn't have a place in software development, since you do want to make sure your app makes sense and is accessible for a human to use, but when precise repeatability is what you need, automation is your friend.

Finally, and I find Go is especially conducive to this, in a big codebase, **tests serve as more documentation**; if the main documentation for a Go package doesn't click for me, my next stop is to look at its test coverage to see real-world usage to a function or interface.

Sounds like some great reasons to write tests, so let's see how to write and run a test in Go!

## 🌺 A slothful "hello world" of Go testing

Let's start by getting some code to test in Go. If you're following along, make a folder titled `sloths` and put the following code into a file `sloths/sloths.go`. This code will tell you whether or not a string `IsSlothful`,

```go
package sloths

import (
    "strings"
)

func IsSlothful(s string) bool {
    if strings.Contains(s, "sloth") {
        return true
    }
    return false
}
```

A string is considered slothful by this function if it contains the word "sloth".

Since we're doing a "hello world" of tests, let's test whether "hello world!" is a slothful string. Since it doesn't contain the word "sloth", we expect that `IsSlothful` will return false. Let's see what a Go test for that looks like. Put the following code in `sloths_test.go`:

```go
package sloths

import (
    "testing"
)

func TestIsSlothful(t *testing.T) {
    if IsSlothful("hello, world!") {
        t.Error("hello, world! is not supposed to be slothful")
    }
}
```

Before we take a look at what's going on in the test, let's try running it. As I mentioned in the beginning, the test runner is built into Go, and what that means is under the `go` command, there is a `test` subcommand that runs the tests.

In the command line, change your working directory to your `sloths` directory, and run `go test -v`. Your command line output should look something like this:

```
=== RUN   TestIsSlothful
--- PASS: TestIsSlothful (0.00s)
PASS
ok      github.com/andyhaskell/orlango-testing-talk/sloths    0.227s
```

Congratulations! You just ran a Go test, and it passed!

## 👀 Taking a closer look at our tests

Now let's see how that test worked. That was a small test function, but it has four things to bring your attention to.

First, take another look at the import block for the Go code. In addition to Go coming with its `go test` test runner out of the box, **the Go standard library includes a `testing` package**.

```go
import (
    "testing"
)
```
The main thing in there you will be working with is its `testing.T` object, which we'll see provides a small set of rules for managing a Go test.

Second, there's the **function signature** to a Go test

```go
func TestIsSlothful(t *testing.T) {
```

All Go test functions have two things in common: they take in a `testing.T` as their only argument, and `Test` is the first camelCase word of their name. This tells the `go test` command which functions it needs to run in a Go package.

Third, let's take a look at how we write assertions in Go. In languages like JavaScript or Ruby, a test that we expect IsSlothful to return false for "hello, world!" would look something like these, reading like a sentence in English:

```javascript
expect(IsSlothful('hello, world!')).toBeFalse();
```

If `IsSlothful` returns true, then the `expect` fails, causing the test to fail.

In Go on the other hand, **a test fails if its testing.T is made to fail**. Our assertion was written in Go like this:

```go
if IsSlothful("hello, world!") {
    t.Error("hello, world! is not supposed to be slothful")
}
```

If `IsSlothful` were to return true, then we call our T's `Error` method, causing the `testing.T` to fail with an error message explaining what went wrong. As you can see, this is a regular Go if statement; **Go testing ultimately is Go code that happens to use the testing package**.

In addition to `t.Error`, there's three other ways to make a `testing.T` fail:

* `t.Fail`, which is like `t.Error`, but it causes the `testing.T` to fail without an error message
* `t.Fatal`, which is like `t.Error`, but makes the test stop immediately
  * This makes `Fatal` good for when you get an error where the results of code in the test past that point wouldn't make sense (for example if a preliminary step fails).
* `t.FailNow`, which is like `t.Fail`, but it stops the test immediately.

The fourth and final thing I would like to draw your attention to is the directory structure we have so far.

```
.
├── sloths.go
└── sloths_test.go
```

**All test files in Go have a name that ends with the `_test.go` suffix**, which tells the `go test` command line tool where to find the test code for your Go project.

The other thing this does is for the coder. Because `go test` requires all your test files to be named this way, that means if you join an engineering team that does Go, or you start contributing to an open source project in Go, **right on day 1** you can find where all the testing code is!

Now that we took a closer look at Go tests, let's try giving our `IsSlothful` function some more test coverage.

## 🐛 Catching a bug with a Go test

We've got our `TestIsSlothful` function, but it could use some more assertions. When you're writing test coverage, you want to test each general category of scenario your code works with. So for `IsSlothful`, that's:

* Passing in a string containing the word "sloth", which should return true
* Passing in a string not containing the word "sloth", which should return false as we already tested

So let's give our `TestIsSlothful` function some more assertions.

```go
if !IsSlothful("hello, slothful world!") {
    t.Error("hello, slothful world! is supposed to be slothful")
}

if !IsSlothful("Sloths rule!") {
    t.Error("Sloths rule! is supposed to be slothful")
}
```

We're testing the strings `hello, slothful world!`, and `Sloths rule!`, expecting our IsSlothful function to return true for both of them.

Let's see if it does by running `go test -v` again.

```
=== RUN   TestIsSlothful
    sloths_test.go:17: Sloths rule! is supposed to be slothful
--- FAIL: TestIsSlothful (0.00s)
exit status 1
FAIL    github.com/andyhaskell/orlango-testing-talk/sloths    0.207s
```

The test failed, but that means our test did its job; it found a bug in our `IsSlothful` function; the string "Sloths rule!" is not considered a slothful string, even though it contains the word "sloth".

Looking at our code, the reason why is because we were passing the string into `strings.Contains` to look for the word "sloth" in all-lowercase. So an uppercase "Sloths rule!" goes undetected; we should have compared the string passed in with the word "sloth", case-insensitive. And luckily, the Go `strings` package has a function for fixing that. Let's check that out.

```diff
  func IsSlothful(s string) bool {
+     s = strings.ToLower(s)
+
      if strings.Contains(s, "sloth") {
          return true
```

Before going into the if statements, we make our string all-lowercase. Now try `go test -v` again and your test should pass!

## 🏗 Trying out some Go test-driven development

With our `IsSlothful` function now having plenty of test coverage, we're in a good place to give it some more logic. And we need that because there's more slothful strings out there.

Sloths love eating hibiscus flowers, so a string with the hibiscus emoji is slothful. But sloths are also laid-back and not in a rush, so if a string has the hibiscus emoji but also the race car emoji, that string isn't slothful.

To try out a test-driven development workflow, let's start adding to our testing logic in `sloths_test.go` rather than `sloths.go`.

```go
func TestHibiscusEmoji(t *testing.T) {
    if !IsSlothful("Nothing like an iced hibiscus tea! 🌺") {
        t.Error("Nothing like an iced hibiscus tea! 🌺 " +
            "is supposed to be slothful")
    }

    if IsSlothful("Get your 🌺 flowers! They're going fast! 🏎️") {
        t.Error("Get your 🌺 flowers! They're going fast! 🏎️ " +
            "is not supposed to be slothful")
    }
}
```

We're testing that we expect the string "Nothing like an iced hibiscus tea! 🌺" to be slothful, but that we expect "Get your 🌺 flowers! They're going fast! 🏎️" not to be slothful.

Run `go test -v` again and the results should look like this:

```
=== RUN   TestIsSlothful
--- PASS: TestIsSlothful (0.00s)
=== RUN   TestHibiscusEmoji
    sloths_test.go:22: Nothing like an iced hibiscus tea! 🌺 is supposed to be slothful
--- FAIL: TestHibiscusEmoji (0.00s)
exit status 1
FAIL    github.com/andyhaskell/orlango-testing-talk/sloths    0.234s
```

Our test failed again, but in TDD that's a good thing; in TDD we see our tests fail so that we know a test passed specifically **because** of our code change.

For the fix, we need to add an extra if statement to indicate that one more scenario where a string is slothful is when it contains the hibiscus emoji, but not the race car emoji.

```diff
  func IsSlothful(s string) bool {
      s = strings.ToLower(s)
+     slothsLikeThis := strings.Contains(s, "🌺") &&
+         !strings.Contains(s, "🏎️")
  
      if strings.Contains(s, "sloth") {
          return true
+     } else if slothsLikeThis {
+         return true
      }
      return false
  }
```

With our new piece of logic added, we're ready to try the test again.

Run `go test -v` one more time and...

```
=== RUN   TestIsSlothful
--- PASS: TestIsSlothful (0.00s)
=== RUN   TestHibiscusEmoji
--- PASS: TestHibiscusEmoji (0.00s)
PASS
ok      github.com/andyhaskell/orlango-testing-talk/sloths    0.245s
```

All our tests passed; our refactor is complete!

By the way, if you have a lot of tests and want to just run a few, you can use the `-run` flag on go test, which only runs tests matching the regular expression passed into the `-run` flag.

For example, to run just TestHibiscus, you can run `go test -v -run Hibiscus`.

## 🗄 A table-testing refactor

So far in our Go tests, we have two test functions running five assertions, but they all have the same structure; we pass in a string to `IsSlothful` and check that the returned boolean is what we expected; if we get back the wrong result, then we run `t.Error` causing the test to fail.

Since all our assertions have the same structure, it would be nice if we made a helper function for our assertion so we can run our tests in one big loop. That technique is popular in Go, and it's called **table testing**.

To start, let's write a helper function:

```go
func assertIsSlothful(t *testing.T, s string, expected bool) {
    if IsSlothful(s) != expected {
        if expected {
            t.Errorf("%s is supposed to be slothful", s)
        } else {
            t.Errorf("%s is not supposed to be slothful", s)
        }
    }
}
```

The parameters to our function are:

* The `testing.T`; if we make a helper function that handles assertions, then that function needs access to our `testing.T` object so we can make the test fail if our assertion fails.
* The string we want to test
* The expected result of `IsSlothful`

Inside the helper function, we compare the result of passing our string to `IsSlothful` with the expected value. If the results don't match, we make the fail test with `t.Errorf`.

`Errorf` and `Fatalf` are variations on the `Error` and `Fatal` methods; rather than taking in just a string for our error message, we pass in a template with percent-sign parameters, and then additional arguments for "filling in the blanks", similar to `fmt.Printf` and `fmt.Sprintf`.

Now, let's set up our test cases to loop over.

```go
type isSlothfulTestCase struct {
    str      string
    expected bool
}

var isSlothfulTestCases = []isSlothfulTestCase{
    {str: "hello, world!",                               expected: false},
    {str: "hello, slothful world!",                      expected: true},
    {str: "Sloths rule!",                                expected: true},
    {str: "Nothing like an iced hibiscus tea! 🌺",       expected: true},
    {str: "Get your 🌺 flowers! They're going fast! 🏎️", expected: false},
}
```

We have a slice of `isSlothful` test cases; if one of them fails, then the string will appear in the error message.

We have a pretty simple assertion we're testing, but if you were doing a more complicated assertion, or it isn't quite clear **what** is being tested (like writing test coverage for an obscure edge case), then an additional field you might add to your test case struct might be a `description` string that your error message can include to provide more detail on your test case if it failed.

Now let's see what our new `IsSlothful` function looks like:

```go
func TestIsSlothful(t *testing.T) {
    for _, c := range isSlothfulTestCases {
        assertIsSlothful(t, c.str, c.expected)
    }
}
```

We loop over our `isSlothfulTestCases`, and we run `assertIsSlothful` on each one, printing the strings that failed the test if any did.

If you see code that that gets repeated a lot in your team's tests and essentially makes one logical step of the test, that might be a good opportunity to use a helper function to simplify your tests. Sometimes there might be too much detail in the code you refactor out for a helper function to be worth it. But used reasonably, custom assertions can help make your tests easier to read.

## 🔭 Where to check out next with tests

We've looked at how we can write Go tests with the standard library and the `go test` tool. But testing is a huge topic; when I presented this at OrlanGo, we had about half an hour of interesting roundtable discussion afterward on people's testing techniques. So you might be wondering where to go next.

The good news is, since `testing.T` handles the rules of Go tests, if you have a good grasp of the fundamentals of Go, you're already in a great place to start writing test coverage in Go; tests in Go are essentially Go code that happens to use a `testing.T` struct.

Additionally, because of Go's small set of syntax rules compared to many other languages, if you're going into a Go codebase and the documentation doesn't give you all the answers you're looking for on how to use some code, you can also read the tests for that piece of code. Those often illustrate how the code is intended to be used. Speaking of, contributing automated tests also is an excellent way to contribute to open source!

Also, since Go is especially popular for backend web development, Go's standard library actually provides a testing package specifically for making it easier to test your HTTP handlers and clients. `net/http/httptest` gives you code like an implementation of the `http.ResponseWriter` for checking that you got back the HTTP response you expected. Additionally it even gives you code for setting up an HTTP server your tests can use, which helps a ton when you're testing clients for web APIs.

Finally, outside the standard library, a lot of assertions, like whether two values are equal to each other, or whether a given value is nil, or whether a function errored, are so common that Mat Ryer, a famous Gopher, actually wrote a Go testing package called [Testify](https://github.com/stretchr/testify). Inside it, you'll find a lot of assertions that feel similar to Jest or RSpec matchers. It also has a `suite` package, which helps you run setup and teardown code before each test case or group of test cases. My slides for the talk this blog post came from give a quick look at Testify, and you can find them in [this GitHub repository](https://github.com/andyhaskell/orlango-testing-talk).