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