Testing Code Security in Go

Introduction

Writing tests for a software application before moving it into production is an important last step in the software delivery system. Usually, this falls into the responsibility of the DevOps engineers, Server Administrators, or Security Engineers if there are one.

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 make the users happy.

Understanding Software Testing

Testing your code simply means making sure that each function or block of code behaves exactly as planned. The Go compiler supports testing and benchmarking with the go test testing framework explicitly. Testing will become an easy thing for you 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 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 as Golang. Go provides testing support in two ways:

  • The testing package
  • The go 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:

  • A test file is named 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
  • the go test command is used 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 go test. You can also write automated tests or even run tests in pipelines.

Unit Testing

Unit testing is the act of testing pieces of a software/program. Unit tests may be written 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

Asides from these categories, unit tests can also be described as positive-path — or focused on standard executions without errors or negative-path — or focused 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 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 Testing

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

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 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

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 different solutions to a single problem.

The Go testing framework has a test_examples/bench 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 starts 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

Software testing is a deep security topic that cannot be treated in a single article. Software unit testing only shows the presence of bugs, it doesn’t show the absence of bugs. Quality code is important in software development, hence you must make it a habit of writing tests for your functions. It is a good thing that Go has robust testing support. Leverage the standard library to make your production software bug free.