Use of go test (specify a test, benchmark, testdata, cover, benchmark...)

Posted by jocknerd on Fri, 21 Jan 2022 03:09:33 +0100

brief introduction

This article will not say much about the role and necessity of testing.
It is very convenient to test in go language. The official provides a relatively perfect test method, using go test command and testing standard library.

Basic usage

The directory structure is shown in the figure below
The source file and the test file are placed in the same directory, and the test file is_ At the end of test, this is a fixed format. When compiling with go build_ The test file will not compile.
Test functions also pay attention to:

  1. Each Test function needs to be prefixed with Test, for example: Test_Division, TestDivision
  2. Each performance test function needs to be prefixed with Benchmark, for example: Benchmark_Division, BenchmarkDivision

Due to the space problem, the source code takes up a lot of space last.

go test parameter

Print all the details of the test function -v

go test -v

Specifies to run a test function - run

go test -run regexp

Run only functions that match regexp
Example:

Performance test bench

go test -bench regexp

go test -bench . "." means to execute all performance test functions under the package

Memory test benchmem

go test -bench="." -benchmem


"16 B/op" means that each call needs to allocate 16 bytes, "1 allocs/op" means that each call has an allocation

Custom test time - benchtime

go test -v -bench=. -benchtime=5s

The default test time of the benchmark framework is 1s. You can specify the test time through the - benchtime parameter.

Enable coverage test - cover

go test -cover

When there is a circular reference problem, create the test package xxx_test to test

For example: net/url package, which provides the function of URL parsing; net/http package, which provides the functions of web service and HTTP client. As expected, the upper net/http package depends on the lower net/url package. Then, one test in the net/url package is to demonstrate the interaction between different URLs and HTTP clients.

In other words, the test code of a lower package is imported into the upper package. Such behavior will lead to package circular dependency in the test code of net/url package. go language specification prohibits package circular dependency.

At this point, you can declare an independent URL in the directory where the net/url package is located_ Test test package (_testis a fixed end format, which is used to tell the go test tool that it should establish an additional package to run the test), so as to avoid the problem of circular reference.

testdata directory

This is also a special directory. When compiling go build, the testdata directory will be automatically ignored, and when running the go test command, the directory where the test file is located will be set as the root directory. You can directly use the relative path testdata to import or store relevant files.

In short, the use scenario of testdata directory is to find out whether the test results meet the expectations through the comparison of file contents, which is applicable to the situation where the input and output are complex.

go official standard library cmd/gofmt/gofmt_test.go source code is useful for reference.

Test with tag

go test -tags="tagName"

Less used.
This rule is often used for integration testing or in combination with version control.

Skip test (skip method)

Example: test according to environment variables

// Use environment variables to test
func TestDivison(t *testing.T) {
	divAddr := os.Getenv("DIV_ADDR")
	if divAddr == "" {
		t.Skip("set DIV_ADDR to run this test")   //The Skip method skips the current test
	}

	t.Log("do DIV_ADDR test")
}

Multithreaded test Parallel

By using t.Parallel() in the test function to mark that the current test function is in parallel test mode, t.Parallel() will reset the test time, which is usually called first in the test function body. The number of parallel tests is affected by the GOMAXPROCS variable. You can also specify the number of parallel tests by going test - parallel n.

The performance of parallel testing needs to be tested. When the test rules are complex and the test time is long, the effect may be obvious. For ordinary test functions, the efficiency may be reduced.

func TestParallel(t *testing.T) {
 t.Parallel()
 // actual test...
}

Table driven test

The most used test examples in the official standard library of go

func TestSplit(t *testing.T) {
	//The official standard library likes to write variables outside the Test function body, which is more conducive to code reading and modification
	//Declare a map of a structure, and use string as a key to distinguish different test cases. The struct structure contains relevant fields for testing, which can be defined freely.
	tests := map[string]struct {
		input string
		sep   string
		want  []string
	}{
		//Using the map structure, you can easily add or delete test cases
		"simple":       {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},
		"wrong sep":    {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},
		"no sep":       {input: "abc", sep: "/", want: []string{"abc"}},
		"trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {  //name is very important here. Otherwise, I only know the error, but I don't know which of the above four test cases is wrong.
			got := strings.Split(tc.input, tc.sep)
			if !reflect.DeepEqual(tc.want, got) {
				t.Fatalf("expected: %v, got: %v", tc.want, got)
				//t.Fail() / / only mark errors without terminating the test
				//t.FailNow() / / mark the error and terminate
			}
		})
	}
}

This test source code

Source code in gitee tTesting package , the project also contains some code used in my daily test. You can have a look at it if you are interested.

hello.go

package tTesting

import "errors"

// reference material
// http://c.biancheng.net/view/124.html
// https://mp.weixin.qq.com/s/HzET8y7lRa7NzJhB49ATtg

func Division(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("Divisor cannot be 0")
	}

	return a / b, nil
}

// This variable is used to test go test
var a string = "a"

hello_test.go

package tTesting

import (
	"os"
	"reflect"
	"strings"
	"testing"
)

// init can also be used here
func init() {
	// os.Setenv("FOO_ADDR", "foo")
}

//gotest_test.go: This is our unit test file, but remember the following principles:
//
//File name must be_ test. Go (the file name must be the type of * _test.go, * represents the file name to be tested), so that the corresponding code will be executed when go test is executed
//You must import testing this package
//All Test case functions must start with Test (the function name must start with Test, such as TestXxx or Test_xxx)
//The test examples will be executed in the order written in the source code
//The parameter of the test function TestXxx() is testing T. We can use this type to record errors or test status
//Test format: func testXXX (t * testing. T). The XXX part can be any alphanumeric combination, but the first letter cannot be lowercase [a-z]. For example, Testintdiv is the wrong function name.
//Function by calling testing T's Error, Errorf, FailNow, Fatal, FatalIf methods indicate that the test fails. Call the Log method to record the test information.

// When the go build command is packaged, the contents in the testdata directory will be automatically ignored, and when the go test command is run, the directory where the test file is located will be set as the root directory,
// You can directly use the relative path testdata to import or store related files, which is a very useful feature. Usually, we will store some large files, such as json files, txt and other text files, under the testdata directory, or as mentioned below golden file.

func Test_Division_1(t *testing.T) {
	if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
		t.Error("Division function test failed") // If it is not as expected, an error will be reported
	} else {
		t.Log("The first test passed") //Record some information you want to record
	}
}
func Test_Division_2(t *testing.T) {
	if _, e := Division(6, 1); e == nil { //try a unit test on function
		t.Log("Division did not work as expected.") // If it is not as expected, an error will be reported
	} else {
		t.Error("one test passed.", e) //Record some information you want to record
	}
}

func TestVar(t *testing.T) {
	t.Log(a)
}

// Use environment variables to test
func TestIntegration(t *testing.T) {
	fooAddr := os.Getenv("FOO_ADDR")
	if fooAddr == "" {
		t.Skip("set FOO_ADDR to run this test")   //The Skip method skips the current test
	}

	t.Log("do FOO_ADDR test")

	//f, err := foo.Connect(fooAddr)
	// ...
}

// Table Driven Test
func TestSplit(t *testing.T) {
	//The official standard library likes to write variables outside the Test function body, which is more conducive to code reading and modification
	//Declare a map of a structure, and use string as a key to distinguish different test cases. The struct structure contains relevant fields for testing, which can be defined freely.
	tests := map[string]struct {
		input string
		sep   string
		want  []string
	}{
		//Using the map structure, you can easily add or delete test cases
		"simple":       {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},
		"wrong sep":    {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},
		"no sep":       {input: "abc", sep: "/", want: []string{"abc"}},
		//"trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {  //name is very important here. Otherwise, I only know the error, but I don't know which of the above four test cases is wrong.
			got := strings.Split(tc.input, tc.sep)
			if !reflect.DeepEqual(tc.want, got) {
				//t.Fatalf("expected: %v, got: %v", tc.want, got)
				t.Fail() // Mark only errors and do not terminate the test
				//t.FailNow() / / mark the error and terminate
			}
		})
	}
}

hello_b_test.go

package tTesting

import (
	"fmt"
	"testing"
)

func Benchmark_Division(b *testing.B) {
	for i := 0; i < b.N; i++ { //use b.N for looping
		Division(4, 5)
	}
}

func Benchmark_TimeConsumingFunction(b *testing.B) {
	b.StopTimer() //The count of times this function was called to stop the stress test

	//Do some initialization work, such as reading file data, database connection, etc,
	//In this way, these times do not affect the performance of our test function itself

	b.StartTimer() //Restart time
	for i := 0; i < b.N; i++ {
		Division(4, 5)
	}
}

// Memory test
// go test -bench="." -benchmem
func Benchmark_Alloc(b *testing.B) {
	for i := 0; i < b.N; i++ {
		fmt.Sprintf("%d", i)
	}
}

reference resources

go language Bible - Test
http://c.biancheng.net/view/124.html
https://mp.weixin.qq.com/s/HzET8y7lRa7NzJhB49ATtg

Topics: Go