A Go test file is simply a set of functions with the following signature:
func Test<Name>(*testing.T)
Here, <Name> is an arbitrary name that reflects the purpose of the test. The test functions are intended to exercise a specific functional unit (or unit test) of the source code.
Before we write the test functions, let us review the code that will be tested. The following source snippet shows a simple implementation of a mathematical vector with Add
, Sub
, and Scale
methods (see the full source code listed at https://github.com/vladimirvivien/learning-go/ch12/vector/vec.go). Notice that each method implements a specific behavior as a unit of functionality, which will make it easy to test:
type Vector interface { Add(other Vector) Vector Sub(other Vector) Vector Scale(factor float64) ... } func New(elems ...float64) SimpleVector { return SimpleVector(elems) } type SimpleVector []float64 func (v SimpleVector) Add(other Vector) Vector { v.assertLenMatch(other) otherVec := other.(SimpleVector) result := make([]float64, len(v)) for i, val := range v { result[i] = val + otherVec[i] } return SimpleVector(result) } func (v SimpleVector) Sub(other Vector) Vector { v.assertLenMatch(other) otherVec := other.(SimpleVector) result := make([]float64, len(v)) for i, val := range v { result[i] = val - otherVec[i] } return SimpleVector(result) } func (v SimpleVector) Scale(scale float64) { for i := range v { v[i] = v[i] * scale } } ...
golang.fyi/ch12/vector/vec.go
The test source code in file vec_test.go
defines a series of functions that exercise the behavior of type SimpleVector
(see the preceding section) by testing each of its methods independently:
import "testing" func TestVectorAdd(t *testing.T) { v1 := New(8.218, -9.341) v2 := New(-1.129, 2.111) v3 := v1.Add(v2) expect := New( v1[0]+v2[0], v1[1]+v2[1], ) if !v3.Eq(expect) { t.Logf("Addition failed, expecting %s, got %s", expect, v3) t.Fail() } t.Log(v1, "+", v2, v3) } func TestVectorSub(t *testing.T) { v1 := New(7.119, 8.215) v2 := New(-8.223, 0.878) v3 := v1.Sub(v2) expect := New( v1[0]-v2[0], v1[1]-v2[1], ) if !v3.Eq(expect) { t.Log("Subtraction failed, expecting %s, got %s", expect, v3) t.Fail() } t.Log(v1, "-", v2, "=", v3) } func TestVectorScale(t *testing.T) { v := New(1.671, -1.012, -0.318) v.Scale(7.41) expect := New( 7.41*1.671, 7.41*-1.012, 7.41*-0.318, ) if !v.Eq(expect) { t.Logf("Scalar mul failed, expecting %s, got %s", expect, v) t.Fail() } t.Log("1.671,-1.012, -0.318 Scale", 7.41, "=", v) }
golang.fyi/ch12/vector/vec_test.go
As shown in the previous code, all test source code must import the "testing"
package. This is because each test function receives an argument of type *testing.T
as its parameter. As is discussed further in the chapter, this allows the test function to interact with the Go test runtime.
It is crucial to realize that each test function should be idempotent, with no reliance on any previously saved or shared states. In the previous source code snippet, each test function is executed as a standalone piece of code. Your test functions should not make any assumption about the order of execution as the Go test runtime makes no such guarantee.
The source code of a test function usually sets up an expected value, which is pre-determined based on knowledge of the tested code. That value is then compared to the calculated value returned by the code being tested. For instance, when adding two vectors, we can calculate the expected result using the rules of vector additions, as shown in the following snippet:
v1 := New(8.218, -9.341) v2 := New(-1.129, 2.111) v3 := v1.Add(v2) expect := New( v1[0]+v2[0], v1[1]+v2[1], )
In the preceding source snippet, the expected value is calculated using two simple vector values, v1
and v2
, and stored in the variable expect
. Variable v3
, on the other hand, stores the actual value of the vector, as calculated by the tested code. This allows us to test the actual versus the expected, as shown in the following:
if !v3.Eq(expect) { t.Log("Addition failed, expecting %s, got %s", expect, v3) t.Fail() }
In the preceding source snippet, if the tested condition is false
, then the test has failed. The code uses t.Fail()
to signal the failure of the test function. Signaling failure is discussed in more detail in the Reporting failure section.
As mentioned in the introductory section of this chapter, test functions are executed using the go test
command-line tool. For instance, if we run the following command from within the package vector, it will automatically run all of the test functions of that package:
$> cd vector $> go test . ok github.com/vladimirvivien/learning-go/ch12/vector 0.001s
The test can also be executed by specifying a sub-package (or all packages with package wildcard ./...
) relative to where the command is issued, as shown in the following:
$> cd $GOPATH/src/github.com/vladimirvivien/learning-go/ch12/ $> go test ./vector ok github.com/vladimirvivien/learning-go/ch12/vector 0.005s
During the development of a large set of test functions, it is often desirable to focus on a function (or set of functions) during debugging phases. The Go test command-line tool supports the -run
flag, which specifies a regular expression that executes only functions whose names match the specified expression. The following command will only execute test function TestVectorAdd
:
$> go test -run=VectorAdd -v === RUN TestVectorAdd --- PASS: TestVectorAdd (0.00s) PASS ok github.com/vladimirvivien/learning-go/ch12/vector 0.025s
The use of the -v
flag confirms that only one test function, TestVectorAdd
, has been executed. As another example, the following executes all test functions that end with VectorA.*$
or match function name TestVectorMag
, while ignoring everything else:
> go test -run="VectorA.*$|TestVectorMag" -v === RUN TestVectorAdd --- PASS: TestVectorAdd (0.00s) === RUN TestVectorMag --- PASS: TestVectorMag (0.00s) === RUN TestVectorAngle --- PASS: TestVectorAngle (0.00s) PASS ok github.com/vladimirvivien/learning-go/ch12/vector 0.043s
When writing new or debugging existing test functions, it is often helpful to print information to a standard output. Type testing.T
offers two logging methods: Log
, which uses a default formatter, and Logf
, which formats its output using formatting verbs (as defined in package to fmt
). For instance, the following test function snippet from the vector example shows the code logging information with t.Logf("Vector = %v; Unit vector = %v
", v, expect)
:
func TestVectorUnit(t *testing.T) { v := New(5.581, -2.136) mag := v.Mag() expect := New((1/mag)*v[0], (1/mag)*v[1]) if !v.Unit().Eq(expect) { t.Logf("Vector Unit failed, expecting %s, got %s", expect, v.Unit()) t.Fail() } t.Logf("Vector = %v; Unit vector = %v ", v, expect) }
golang.fyi/ch12/vector/vec_test.go
As seen previously, the Go test tool runs tests with minimal output unless there is a test failure. However, the tool will output test logs when the verbose flag
-v
is provided. For instance, running the following in package vector will mute all logging statements:
> go test -run=VectorUnit PASS ok github.com/vladimirvivien/learning-go/ch12/vector 0.005s
When the verbose flag -v
is provided, as shown in the following command, the test runtime prints the output of the logs as shown:
$> go test -run=VectorUnit -v === RUN TestVectorUnit --- PASS: TestVectorUnit (0.00s) vec_test.go:100: Vector = [5.581,-2.136]; Unit vector = [0.9339352140866403,-0.35744232526233] PASS ok github.com/vladimirvivien/learning-go/ch12/vector 0.001s
By default, the Go test runtime considers a test a success if the test function runs and returns normally without a panic. For example, the following test function is broken, since its expected value is not properly calculated. The test runtime, however, will always report it as passing because it does not include any code to report the failure:
func TestVectorDotProd(t *testing.T) { v1 := New(7.887, 4.138).(SimpleVector) v2 := New(-8.802, 6.776).(SimpleVector) actual := v1.DotProd(v2) expect := v1[0]*v2[0] - v1[1]*v2[1] if actual != expect { t.Logf("DotPoduct failed, expecting %d, got %d", expect, actual) } }
golang.fyi/ch12/vec_test.go
This false positive condition may go unnoticed, especially if the verbose flag is turned off, minimizing any visual clues that it is broken:
$> go test -run=VectorDot PASS ok github.com/vladimirvivien/learning-go/ch12/vector 0.001s
One way the previous test can be fixed is by using the Fail
method from type testing.T
to signal failure, as shown in the following snippet:
func TestVectorDotProd(t *testing.T) { ... if actual != expect { t.Logf("DotPoduct failed, expecting %d, got %d", expect, actual) t.Fail() } }
So now, when the test is executed, it correctly reports that it is broken, as shown in the following output:
$> go test -run=VectorDot --- FAIL: TestVectorDotProd (0.00s) vec_test.go:109: DotPoduct failed, expecting -97.460462, got -41.382286 FAIL exit status 1 FAIL github.com/vladimirvivien/learning-go/ch12/vector 0.002s
It is important to understand that method Fail
only reports failure and does not halt the execution of a test function. On the other hand, when it makes sense to actually exit the function upon a failed condition, the test API offers the method FailNow
, which signals failure and exits the currently executing test function.
Type testing.T
provides the convenience methods Logf
and Errorf
, which combine both logging and failure reporting. For instance, the following snippet uses the Errorf
method, which is equivalent to calling the Logf
and Fail
methods:
func TestVectorMag(t *testing.T) { v := New(-0.221, 7.437) expected := math.Sqrt(v[0]*v[0] + v[1]*v[1]) if v.Mag() != expected { t.Errorf("Magnitude failed, execpted %d, got %d", expected, v.Mag()) } }
golang.fyi/ch12/vector/vec.go
Type testing.T
also offers Fatal
and Formatf
methods as a way of combining the logging of a message and the immediate termination of a test function.
It is sometimes necessary to skip test functions due to a number of factors such as environment constraints, resource availability, or inappropriate environment settings. The testing API makes it possible to skip a test function using the SkipNow
method from type testing.T
. The following source code snippet will only run the test function when the arbitrary operating system environment variable named RUN_ANGLE
is set. Otherwise, it will skip the test:
func TestVectorAngle(t *testing.T) { if os.Getenv("RUN_ANGLE") == "" { t.Skipf("Env variable RUN_ANGLE not set, skipping:") } v1 := New(3.183, -7.627) v2 := New(-2.668, 5.319) actual := v1.Angle(v2) expect := math.Acos(v1.DotProd(v2) / (v1.Mag() * v2.Mag())) if actual != expect { t.Logf("Vector angle failed, expecting %d, got %d", expect, actual) t.Fail() } t.Log("Angle between", v1, "and", v2, "=", actual) }
Notice the code is using the Skipf
method, which is a combination of the methods SkipNow
and Logf
from type testing.T
. When the test is executed without the environment variable, it outputs the following:
$> go test -run=Angle -v === RUN TestVectorAngle --- SKIP: TestVectorAngle (0.00s) vec_test.go:128: Env variable RUN_ANGLE not set, skipping: PASS ok github.com/vladimirvivien/learning-go/ch12/vector 0.006s
When the environment variable is provided, as is done with the following Linux/Unix command, the test executes as expected (consult your OS on how to set environment variables):
> RUN_ANGLE=1 go test -run=Angle -v === RUN TestVectorAngle --- PASS: TestVectorAngle (0.00s) vec_test.go:138: Angle between [3.183,-7.627] and [-2.668,5.319] = 3.0720263098372476 PASS ok github.com/vladimirvivien/learning-go/ch12/vector 0.005s
One technique you often encounter in Go is the use of table-driven tests. This is where a set of input and expected output is stored in a data structure, which is then used to cycle through different test scenarios. For instance, in the following test function, the cases
variable, of type []struct{vec SimpleVector; expected float64}
, to store several vector values and their expected magnitude values used to test the vector method Mag
:
func TestVectorMag(t *testing.T) { cases := []struct{ vec SimpleVector expected float64 }{ {New(1.2, 3.4), math.Sqrt(1.2*1.2 + 3.4*3.4)}, {New(-0.21, 7.47), math.Sqrt(-0.21*-0.21 + 7.47*7.47)}, {New(1.43, -5.40), math.Sqrt(1.43*1.43 + -5.40*-5.40)}, {New(-2.07, -9.0), math.Sqrt(-2.07*-2.07 + -9.0*-9.0)}, } for _, c := range cases { mag := c.vec.Mag() if mag != c.expected { t.Errorf("Magnitude failed, execpted %d, got %d", c.expected, mag) } } }
golang.fyi/ch12/vector/vec.go
With each iteration of the loop, the code tests the value calculated by the Mag
method against an expected value. Using this approach, we can test several combinations of input and their respective output, as is done in the preceding code. This technique can be expanded as necessary to include more parameters. For instance, a name field can be used to name each case, which is useful when the number of test cases is large. Or, to be even more fancy, one can include a function field in the test case struct to specify custom logic to use for each respective case.
18.119.104.95