Testing Your Code in Golang

Writing tests for a software application before moving it into production is an essential last step in the software delivery system. However, it is expedient that you know how to write highly maintainable unit tests if you are working with production-ready software up close. Tests reduce the possibility of producing software with bugs and vulnerabilities and making users happy.

This article will discuss testing the code in the Go programming language.

Understanding Software Testing

Testing your code means ensuring that each function or block of code behaves exactly as planned. The Go language compiler explicitly supports testing and benchmarking with the Go language test testing framework. Testing will become easy once you get used to integrating tests into your projects and CI systems.

There are different types of testing for software. They include:

  • Unit testing
  • Integration testing
  • Mock testing
  • Smoke testing
  • Regression testing
  • User acceptance testing
  • Others

Go Language Testing Package

When you are about to write tests, the first thing that would come to mind is the library or framework made available by the language the software is written in for testing.

Most programming languages have a testing module or package in their standard library, just like Golang. Go provides testing support in two ways:

  • The testing package
  • The Go programming test tool from the CLI

The Go testing package provides support for ad-hoc and automated testing of your Go code with the following functionalities:

  • We name a test file after the source file it aims to test but with a “_test.go” suffix.
  • Functions that are written in the test file start with a Test or T prefix
  • We use the Golang test command to run test files and generate reports

Go tests are located in the same directory and package as the tested software.

You can run tests directly from the terminal ad-hoc with the Golang test. You can also write automated tests or even run tests in pipelines.

Unit Testing in Go Language

Unit testing is the act of testing pieces of software programs. We can write unit tests to test specific actions, like whether a code executes as expected or produces an expected error.

Testing in Go may be in two forms:

  • Basic unit testing
  • Table unit testing

Aside from these categories, unit tests can also be described as positive-path — or focused on standard executions without errors or negative-path — or on producing an expected error.

Basic Unit Testing

Basic unit testing tests for a single set of parameters or results. Go’s test functions accept only the “*testing.T” parameter and do not return a value. Let’s create an adder folder in a code editor to provide a simple example. Create a file named “addition. Go” and add the following function to sum two numbers:

package adder

import “fmt”

func AddTwoNumbers(x, y int) int {
    return x + y
}

func main() {
    var x, y int
    solution := AddTwoNumbers(x, y)
    fmt.Println(solution)
}

Next, create an “addition_test.go” file and add the following code to it:

package adder

import “testing”

func Test_AddTwoNumbers(t *testing.T) {
    testResult := AddTwoNumbers(10, 15)
    if testResult != 25 {
        t.Error("Results are not correct: expected 25, got ", testResult)
    }
}

Run the Go test command in the repository, and you will see a similar output to:

$ go test
PASS
ok      github.com/theghostmac/golang-unit-testing/adder        0.723s

Testing a Package as a Public API

Tests can also be run on the public API of your complete software package. The name of the test package is given in the format “packagename_test”. The test source code can still be left in the production package. To explain practically, exit the adder package and create an “adderPublic_test” package. Then create a file named “adder_public_test.go” and add the following code to it:

package adderPublic_test

import (
    “github.com/theghostmac/golang-unit-testing/adder”
    “testing”
)

func Test_addTwoNumbers(t *testing.T) {
    testResult := adder.AddTwoNumbers(10, 15)
    if testResult != 5 {
        t.Error("Result is incorrect: expected 5, got ", testResult)
    }
}

Run the command go test -v to see a FAIL message. This is because the function “testResult” specifies 5 as output instead of 25. The -v flag stands for verbose and provides even more:

$ go test -v
=== RUN   Test_AddTwoNumbers
--- PASS: Test_AddTwoNumbers (0.00s)
PASS
ok      github.com/theghostmac/golang-unit-testing/adderPublic_test     1.241s

Table-Driven Testing in Golang

Table unit testing tests for multiple values and results. You should write multiple test functions to validate important functions accurately. Let’s try another example of software, but this time with table testing. The function will have different branches to it to suit the table test. Please check the following code:

package main

import (
    “errors”
    “fmt”
)

func Calculate(num1, num2 int, arithmeticOperations string) (int, error) {
    switch arithmeticOperations {
    case "+":
        return num1 + num2, nil
    case “-”:
        return num1 - num2, nil
    case “*”:
        return num1 * num2, nil
    case "/":
        if num2 == 0 {
            return 0, errors.New("division by zero is undefined")
        }
        return num1 / num2, nil
    default:
        return 0, fmt.Errorf("unknown operation %s ", arithmeticOperations)
    }
}

func main() {
    calculate, err := Calculate(2, 3, "*")
    if err != nil {
        return
    }
    fmt.Println(calculate)
}

To run a normal unit test for this function, we will have to write repetitive code like this:

package main

import “testing”

func TestCalculate(t *testing.T) {
    testResult, err := Calculate(6, 4, "*")
    if testResult != 24 {
        t.Error("Result not correct: expect 4, got ", testResult)
    }
    if err != nil {
        t.Error("Error not nil: got ", err)
    }
    secondTestResult, secondErr := Calculate(16, 4, "/")
    if secondTestResult != 4 {
        t.Error("Result not correct: expect 4, got ", secondTestResult)
    }
    if secondErr != nil {
        t.Error("Error not nil: got ", secondErr)
    }
    // and so forth...
}

Unit testing fails here, as it causes repetition. Instead, delete or rename the test file and create a new named *_test.go file. Let’s see how to write a table test for this function:

package main

import “testing”

func TestCalculate(t *testing.T) {
    outputVariables := []struct {
        operationName       string
        num1                int
        num2                int
        arithmeticOperation string
        expectedValue       int
        errorMessage        string
    }{
        {"addition", 4, 6, "+", 10, ""},
        {"subtraction", 6, 4, "-", 2, ""},
        {"multiplication", 5, 4, "*", 20, ""},
        {"division", 10, 5, "/", 2, ""},
        {"undefined", 5, 0, "/", 0, "division by zero is undefined"},
    }
    for _, valueOf := range outputVariables {
        t.Run(valueOf.operationName, func(t *testing.T) {
            testResult, err := Calculate(valueOf.num1, valueOf.num2, valueOf.arithmeticOperation)
            if testResult != valueOf.expectedValue {
                t.Errorf("Expected %d: got %d", valueOf.expectedValue, testResult)
            }
            var errorMessage string
            if err != nil {
                errorMessage = err.Error()
            }
            if errorMessage != valueOf.errorMessage {
                t.Errorf("Expected error message: `%s`, got `%s` ", valueOf.errorMessage, errorMessage)
            }
        })
    }
}

After running the go test -v command, you get a PASS too.

Benchmark Testing in Golang

Benchmark testing is a different kind of testing used to determine the speed performance of a program. You can use it in scenarios where you want to test the performance of two solutions to a single problem.

The Go programming language testing framework has a benchmarking package for benchmark testing. Here is an example of a benchmark test for counting the characters in a file:

package benchmarking

import “os”

func LengthOfFile(f string, bufferSize int) (int, error) {
    f, err := os.Open(f)
    if err != nil {
        return 0, err
    }
    defer f.Close()

    count := 0
    for {
        buffer := make([]byte, bufferSize)
        num, err := f.Read(buffer)
        count += num
        if err != nil {
            break
        }
    }
    return count, nil
}

We have to write a test for the code above:

package benchmarking

import “testing”

func TestLengthOfFile(t *testing.T) {
    testResult, err := LengthOfFile("sample.txt", 1)
    if err != nil {
        t.Fatal(err)
    }
    if testResult != 500 {
        t.Error("Expected 500, got ", testResult)
    }
}

Now, let’s add the benchmark test function. Benchmark tests are functions in the actual test file that start with the Benchmark keyword instead of Test or T. It accepts a single parameter with the “*testing.B” type.

Append the following code to the file above:

var benchResult int

func BenchmarkLengthOfFile(b *testing.B) {
    for i := 0; i < b.N; i++ {
        benchTestResult, err := LengthOfFile("sample.txt", 1)
        if err != nil {
            b.Fatal(err)
        }
        benchResult = benchTestResult
    }
}

After running the test, you should see the output in five columns, including the B/op value showing the initial number.

Conclusion

In this article, you learned about testing the Go language code. You used the standard tooling provided by Go for unit and benchmark testing. l

Software testing is a deep topic that cannot be treated in a single article. Software unit testing only shows the presence of bugs, and it doesn’t show the absence of bugs.

Quality code is essential in software development. Hence, you must make it a habit of writing tests for your functions. It is a good thing that the Go programming language (Golang) has robust testing support. Leverage the standard library to make your production software bug-free.

MacBobby Chibuzor is a Robotics Hardware Engineer and a Tech Polyglot. He also has practical experience in Software Engineering and Machine Learning, with an interest in embedded systems and Blockchain technology. In addition, Mac loves to spend his free time in technical writing.

Need help?

Let us know about your question or problem and we will reach out to you.