Golang Project Structure

Tutorials, tips and tricks for writing and structuring code in Go (with additional content for other programming languages)

How to Test Code in Go

Language

  • unknown

by

Testing should be considered an essential part of the software-development process, because it helps to ensure that our code works as expected when handling a wide range of reasonable values as input.

A health worker in a laboratory is performing a medical test using high-tech scientific equipment.
Writing tests for code to improve its reliability should be considered just as important as performing medical tests to improve a person’s health.

So today we are going to look at how to write tests in Go, using the built-in syntax, and how to run the tests — as well as how to use other testing frameworks.

Why Bother Writing Tests in Go?

The Go programming language has built-in support for unit testing, which makes it extremely easy to write and run tests for our code.

The central idea behind unit testing is that code can be broken down into small sections, known as units, which may consist of individual functions or methods, and that these can — and should — be tested in insolation from the rest of your application. A function could tested by feeding example inputs into it and checking that the outputs make sense.

By writing tests as we develop our codebase, we can find bugs that may cause run-time errors before they make it into production-ready software.

This also allows us to notice problems that could arise in a function that previously used to work correctly when modifying its code. Whenever major modifications are made to any section of code, it should be tested to ensure that it still meets its requirements successfully.

How to Write Unit Tests in Go

All of the functions that are used to perform unit tests in Go are written in a separate files that each have the suffix _test.go.

For example, if we have a file called calculation.go, then our corresponding test file should be called calculation_test.go.

Test functions are written with the help of the “testing” package, which is part of Go’s standard library.

As an example, let’s define a simple function that we can later write a unit test for:

calculation.go

package calculation

func AddAbs(a, b int) int {
	var sum int

	if a >= 0 {
		sum += a
	} else {
		sum += -a
	}

	if b >= 0 {
		sum += b
	} else {
		sum += -b
	}

	return sum
}

The AddAbs function adds the absolute values of two integers together and returns the sum.

This means that if either of the two integers that are given as input is negative, it will be made positive before it is added to the other one. If both of the integers are positive already, then they will simply be added together.

So the function will always return a positive number.

Now that we’ve created the AddAbs function, let’s write a unit test for it:

calculation_test.go

package calculation

import "testing"

func TestAddAbs(t *testing.T) {
	result := AddAbs(-1, 9)
	if result != 10 {
		t.Errorf("AddAbs(-1, 9) == %d; expected 10", result)
	}

	result = AddAbs(-50, -25)
	if result != 75 {
		t.Errorf("AddAbs(-50, -75) == %d; expected 75", result)
	}
}

The t.Errorf method works like the fmt.Printf function that we discussed in detail recently, except that the output is logged to os.Stderr rather than os.Stdout.

Notice how the calculation_test.go file uses the same package name as the calculation.go.

It is also possible to write test files in a separate package that has a name ending with the suffix _test (so, in this case, calculation_test). However, if we’d done this, we would have needed to import the calculation package in order to use the AddAbs function.

How to Run Unit Tests in Go

In order to run any tests that we have written, we simply run the go test command.

This command looks for all of the test files within the current directory and reads them, running all of the functions within those files that have names starting with the prefix Test (such as TestAddAbs that we defined above).

It’s also possible to run only the tests that match a specific pattern, by giving a pattern as an argument to the -run option as shown in the command below:

go test -run=AddAbs

Introducing Testing Frameworks in Go

While the native "testing" package is sufficient for most purposes, Go also has many testing frameworks that have been created by third parties with the aim of making it easier and more efficient to write tests.

These frameworks often provide additional functionality and features that can help to streamline the entire testing process.

Using the Testify Framework in Go

Testify is one of the most commonly used testing frameworks in Go.

Before using Testify, we should ensure that we have the most recent version installed by running the following command:

go get -u github.com/stretchr/testify

Now let’s rewrite our original test function, using the Testify framework:

package mycode

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestAdd(t *testing.T) {
	result := AddAbs(-1, 9)

	assert.Equal(t, 10, result, "AddAbs(-1, 9) should equal 10")

	result = AddAbs(-50, -25)

	assert.Equal(t, 75, result, "AddAbs(-50, -25) should equal 75")
}

The asset.Equal function simply checks that an expected value equals an actual value and prints a given error message if it doesn’t. This reduces the need for us to include any conditional logic in our code, as we had with the initial test function that we wrote.

Notice, however, how we still use the native "testing" package in order to pass the testing.T pointer as an argument to the assert.Equal function.

What Are Assertions and How Are They Used in Testing?

The assert.Equal function that the Testify framework provides is an example of an assertion.

Assertions are statements in a program that declare — or, in other words, assert — that a certain condition must be true.

Each assertion is used to verify that the behaviour of a section of code works as expected.

Testify’s "assert" package also contains functions such as NotEqual (which declares that two values must not equal one another), Nil (which declares that a value must be nil) and NotNil (which declares that a value must not be nil).

Using the Ginko Testing Framework

Ginkgo is an alternative testing framework that is based on the idea of Behaviour-Driven Development (BDD), providing a more expressive way to write tests.

Behaviour-Driven Development focuses on describing what goals each test should achieve.

This means that Ginkgo allows us to write tests using descriptive language that reads somewhat like English.

Gomega is Ginkgo’s assertion library.

When used together, Ginkgo and Gomega can be thought of as a single domain-specific language (DSL).

We can install the latest versions of Ginkgo and Gomega by running the following commands:

go get -u github.com/onsi/ginkgo/v2
go install github.com/onsi/ginkgo/v2/ginkgo
go get -u github.com/onsi/gomega/...

Now let’s look at an example of the Ginkgo testing framework in action:

package calculation

import (
    "testing"

    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
)

func TestCodeWithGinko(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "Ginko Test Suite")
}

var _ = Describe("AddAbs", func() {
    It("should add the absolute value of two integers together", func() {
        result := AddAbs(-99, 2)

        Expect(result).To(Equal(101))
    })
})

We begin, in the above example, by importing both the Ginkgo and Gomega packages.

We use a dot before the path to each import, because we want the exported functions to act as though they are part of the current package, allowing us to use their names without a qualifier.

This is just a matter of style, so it would have been perfectly possible to import the packages the normal way, but this would have the side effect of making the imported functions read less like plain English, because we’d have to keep repeating the package names.

The Describe function takes as its first argument a string that describes the nature of the tests that occur within the callback function given as its second argument.

Likewise, the It function takes as its first argument a string that describes what should be expected from a specific unit test that occurs within the callback function given as its second argument.

In order to run the Describe function without storing its return value, we assign it to an underscore variable, which is known as the blank identifier.

The Expect and Equal functions are provided by the Gomega library, and they help us to assert an expected result for our unit test. In this case, the test will fail if the result of AddAbs(-99, 2) does not equal 101.

If you want to learn more about the Ginkgo test framework and how it can be used to write more complex sequences of unit tests (by, for example, running a section of setup code before each test), I suggest that you read the excellent documentation provided by Onsi Fakhouri, the developer who created the framework.

Leave a Reply

Your email address will not be published. Required fields are marked *