The importance of writing tests for your software cannot be overemphasized. A school of thought called Test-Driven Development suggests that developers should start building software by writing code first.
There are two approaches to writing tests:
writing tests for a program to check whether the functions in it perform as expected
writing tests during problem-solving to build software that passes the test.
You may adopt both, using each of them whenever it is suitable. Let’s explore both scenarios.
Writing tests for functions
Tests work with the testing
package in Go. Here are a few rules to follow when writing tests:
If you’re testing for a
services.go
file. Your test file name will beservices_test.go
.If you’re testing for a
FetchPrice
function, your test function name will beTestFetchPrice
.If you’re writing tests for a function with another function bearing the same name, you can add the function’s package name to the test function name. For example, if two
FetchPrice
functions exist in separate folders. You can test them separately usingTest_StocksFetchPrice
andTest_CryptoFetchPrice
, where stocks and crypto are different package names.The test function must accept
(t *testing.T)
in the parameter.
Let’s use a simple Add
function to demonstrate. Create an add.go
file:
package AdvancedGo
func Add(a, b int) int {
return a + b
}
Next, create an add_test.go
file with this:
package AdvancedGo_test
import (
"testing"
)
func TestAdd(t *testing.T) {
// test case 1: check if Add() works correctly
testCase1 := []struct {
a, b, expected int
}{
{1, 2, 3},
{0, 0, 0},
{-1, -1, -2},
}
for _, test := range testCase1 {
result := Add(test.a, test.b)
if result != test.expected {
t.Errorf("Add(%d, %d) = %d; want %d", test.a, test.b, result, test.expected)
}
}
Run go test
, and you’ll get an Ok
result. When you have functions that perform serious operations in a program, you should write test cases for when it passes to check correctness and for when it fails. This gives you an idea of how your program behaves when it fails.
Writing tests for fixing things
This approach involves writing tests first and then writing code to pass the test. Let’s see an example with a program that finds the maximum element in an array. Create a max.go
file and a max_test.go
file. In the test file, you can write this:
package main_test
import (
"testing"
)
func TestFindMax(t *testing.T) {
// how FindMax func should behave
testArray1 := []int{1, 2, 3, 4, 5}
testArray2 := []int{6, 7, 8, 9, 10}
testCase1 := []struct {
array []int
maxElement int
}{
{testArray1, 5},
{testArray2, 10},
}
for _, test := range testCase1 {
result := FindMax(test.array)
if result != test.maxElement {
t.Errorf("FindMax(%v) = %d; want %d", test.array, result, test.maxElement)
}
}
}
If you run the tests using the go test
command, you will get the following output:
./max_test.go:20:13: undefined: FindMax
FAIL github.com/theghostmac/Backend-Engineering-With-Go/AdvancedGo [build failed]
This means we need the FindMax function. Let’s go ahead and create it in the max.go
file with the minimal code possible:
package main
func FindMax(array []int) int {
return 0
}
If you run it, you get:
--- FAIL: TestFindMax (0.00s)
max_test.go:23: FindMax([1 2 3 4 5]) = 0; want 5
max_test.go:23: FindMax([6 7 8 9 10]) = 0; want 10
FAIL
exit status 1
FAIL /path/to/project/ 4.535s
Next, we try to optimize the function to pass the test:
package main
func FindMax(array []int) int {
max := array[0]
for _, num := range array {
if num > max {
max = num
}
}
return max
}
Now run the test:
go test
PASS
ok path/to/project/ 2.439s
Great! It passes. This type of testing is suitable for building libraries and packages that are not run.
Note that
Exercise
Write unit tests for the functions in our dummy stock prices program above.
Read about different types of tests adopted in software engineering and when to use which. Proceed to learn how Go does whichever test you believe you should learn to use in your code.