Unit test of Go language foundation

Development without writing tests is not a good programmer. I personally advocate TDD (Test Driven Development), but unf...
Format of test function
Test function example
Test group
Sub test
Test coverage
Benchmark function format
Benchmark example
Performance comparison function
reset time
Parallel test
TestMain
Setup and Teardown of subtests
Format of sample function
Example function example

Development without writing tests is not a good programmer. I personally advocate TDD (Test Driven Development), but unfortunately, domestic programmers do not pay much attention to the test part. This article mainly introduces how to do unit test and benchmark in Go language.

go test tool

Tests in the Go language rely on the go test command. Writing test code is similar to writing normal Go code, and does not require learning new syntax, rules, or tools.

The go test command is a driver of test code organized according to certain conventions. In the package directory, all the source code files with Suffix "_test.go" are part of the go test test, and will not be compiled into the final executable file by go build.

There are three types of functions in the * * test.go file: unit test function, benchmark function and sample function.

type format Effect Test function Function name prefix is Test Test whether some logical behaviors of the program are correct Datum function Function name prefix is Benchmark Test function performance Example function Function name prefix is Example Provide sample documents for documents

The go test command will traverse all the functions in the * _test.go file that meet the above naming rules, then generate a temporary main package to call the corresponding test functions, then build and run, report the test results, and finally clean up the temporary files generated in the test.

Test function

Format of test function

Each test function must be imported into the testing package. The basic format (signature) of the test function is as follows:

func TestName(t *testing.T){ // ... }

The name of the Test function must start with Test, and the optional suffix must start with an uppercase letter. For example:

func TestAdd(t *testing.T){ ... } func TestSum(t *testing.T){ ... } func TestLog(t *testing.T){ ... }

Parameter t is used to report test failure and additional log information. The methods of testing.T are as follows:

func (c *T) Error(args ...interface{}) func (c *T) Errorf(format string, args ...interface{}) func (c *T) Fail() func (c *T) FailNow() func (c *T) Failed() bool func (c *T) Fatal(args ...interface{}) func (c *T) Fatalf(format string, args ...interface{}) func (c *T) Log(args ...interface{}) func (c *T) Logf(format string, args ...interface{}) func (c *T) Name() string func (t *T) Parallel() func (t *T) Run(name string, f func(t *T)) bool func (c *T) Skip(args ...interface{}) func (c *T) SkipNow() func (c *T) Skipf(format string, args ...interface{}) func (c *T) Skipped() bool

Test function example

Just as cells are the basic units that make up our bodies, a software program is also made up of many unit components. Unit components can be functions, structures, methods, and anything the end user might depend on. All in all, we need to make sure that these components are working properly. Unit tests are programs that use various methods to test unit components, which compare results with expected output.

Next, we define a split package, in which a split function is defined. The specific implementation is as follows:

// split/split.go package split import "strings" // split package with a single split function. // Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+1:] i = strings.Index(s, sep) } result = append(result, s) return }

In the current directory, we create a test file of split_test.go, and define a test function as follows:

// split/split_test.go package split import ( "reflect" "testing" ) func TestSplit(t *testing.T) { // The Test function name must start with Test and must receive a * testing.T type parameter got := Split("a:b:c", ":") // Results of program output want := []string{"a", "b", "c"} // Expected results if !reflect.DeepEqual(want, got) { // Because slice can't be compared directly, we use the method in reflection packet to compare t.Errorf("excepted:%v, got:%v", want, got) // Test failure output error prompt } }

The files in the split package are as follows:

split $ ls -l total 16 -rw-r--r-- 1 liwenzhou staff 408 4 29 15:50 split.go -rw-r--r-- 1 liwenzhou staff 466 4 29 16:04 split_test.go

Under the split package path, execute the go test command, and the output results are as follows:

split $ go test PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s

A test case is a bit thin. Let's write another example of using multiple characters to cut strings. Add the following test functions to split_test.go:

func TestMoreSplit(t *testing.T) { got := Split("abcd", "bc") want := []string{"a", "d"} if !reflect.DeepEqual(want, got) { t.Errorf("excepted:%v, got:%v", want, got) } }

Run the go test command again, and the output is as follows:

split $ go test --- FAIL: TestMultiSplit (0.00s) split_test.go:20: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s

This time, our test failed. We can add the - v parameter to the go test command to view the name and running time of the test function:

split $ go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestMoreSplit --- FAIL: TestMoreSplit (0.00s) split_test.go:21: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s

This time, we can see clearly that the test moreplit is not successful. You can also add the - run parameter after the go test command, which corresponds to a regular expression. Only the test function matching the function name will be executed by the go test command.

split $ go test -v -run="More" === RUN TestMoreSplit --- FAIL: TestMoreSplit (0.00s) split_test.go:21: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s

Now let's go back and solve the problems in our program. Obviously, our original split function didn't take into account the fact that sep is multiple characters. Let's fix this Bug:

package split import "strings" // split package with a single split function. // Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+len(sep):] // Here, use len(sep) to get the length of sep i = strings.Index(s, sep) } result = append(result, s) return }

Let's test our program again this time. Note that when we modify our code, we should not only execute those failed test functions, we should run all tests completely to ensure that no new problems will be introduced due to code modification.

split $ go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestMoreSplit --- PASS: TestMoreSplit (0.00s) PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s

This time we passed all our tests.

Test group

Now we also want to test the support of split function for Chinese strings. At this time, we can write another TestChineseSplit test function, but we can also use the following more friendly way to add more test cases.

func TestSplit(t *testing.T) { // Define a test case type type test struct { input string sep string want []string } // Define a slice to store test cases tests := []test{ }, }, }, }, } // Traverse slices and execute test cases one by one for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%v, got:%v", tc.want, got) } } }

We use the above code to combine multiple test cases and execute the go test command again.

split $ go test -v === RUN TestSplit --- FAIL: TestSplit (0.00s) split_test.go:42: excepted:[River has River], got:[ River has River] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s

There is a problem in our test. Take a close look at the printed test failure prompt message: excluded: [River has River], got: [River has River], you will find that there is an inconspicuous empty string in [River has River], in this case,% ාv format is recommended.

We modify the format output error prompt of the following test case:

func TestSplit(t *testing.T) { ... for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%#v, got:%#v", tc.want, got) } } }

After running the go test command, you can see the obvious prompt message:

split $ go test -v === RUN TestSplit --- FAIL: TestSplit (0.00s) split_test.go:42: excepted:[]string{"River has", "Another river"}, got:[]string{"", "River has", "Another river"} FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s

Sub test

It seems to be pretty good, but if there are many test cases, we can't see which test case failed at a glance. We may come up with the following solutions:

func TestSplit(t *testing.T) { type test struct { // Define test structure input string sep string want []string } tests := map[string]test{ // Test cases using map storage "simple": }, "wrong sep": }, "more sep": }, "leading sep": }, } for name, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("name:%s excepted:%#v, got:%#v", name, tc.want, got) / / format the name of the test case as output } } }

The above approach can solve the problem. At the same time, sub tests are added in Go1.7 +, we can use t.Run to execute sub tests as follows:

func TestSplit(t *testing.T) { type test struct { // Define test structure input string sep string want []string } tests := map[string]test{ // Test cases using map storage "simple": }, "wrong sep": }, "more sep": }, "leading sep": }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // Use t.Run() to perform subtests got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%#v, got:%#v", tc.want, got) } }) } }

At this point, we can see a clearer output after executing the go test command:

split $ go test -v === RUN TestSplit === RUN TestSplit/leading_sep === RUN TestSplit/simple === RUN TestSplit/wrong_sep === RUN TestSplit/more_sep --- FAIL: TestSplit (0.00s) --- FAIL: TestSplit/leading_sep (0.00s) split_test.go:83: excepted:[]string{"River has", "Another river"}, got:[]string{"", "River has", "Another river"} --- PASS: TestSplit/simple (0.00s) --- PASS: TestSplit/wrong_sep (0.00s) --- PASS: TestSplit/more_sep (0.00s) FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s

At this time, we need to correct the errors in the test cases:

func TestSplit(t *testing.T) { ... tests := map[string]test{ // Test cases using map storage "simple": }, "wrong sep": }, "more sep": }, "leading sep": }, } ... }

We all know that you can use - run=RegExp to specify the test cases to run and / to specify the sub test cases to run. For example: go test -v -run=Split/simple will only run the sub test cases corresponding to simple.

Test coverage

Test coverage is the percentage of your code covered by the test suite. We usually use statement coverage, which is the proportion of code that is run at least once in the test to the total code.

Go provides built-in functionality to check your code coverage. We can use go test cover to see the test coverage. For example:

split $ go test -cover PASS coverage: 100.0% of statements ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s

From the above results, we can see that our test case covers 100% of the code.

Go also provides an additional - coverprofile parameter to output coverage related record information to a file. For example:

split $ go test -cover -coverprofile=c.out PASS coverage: 100.0% of statements ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s

The above command will output the coverage related information to the c.out file under the current folder. Then we execute go tool cover -html=c.out, and use the cover tool to process the generated record information. The command will open the local browser window to generate an HTML report. In the above figure, each statement block marked with green indicates that it is covered, while the red indicates that it is not.

Benchmark test

Benchmark function format

Benchmarking is a way to check the performance of a program under a certain workload. The basic format of benchmark is as follows:

func BenchmarkName(b *testing.B){ // ... }

Benchmark is prefixed with benchmark, and requires a parameter b of type * testing.B. benchmark must be executed b.N times, so that the test can be compared. The value of b.N is adjusted by the system according to the actual situation, so as to ensure the stability of the test. testing.B has the following methods:

func (c *B) Error(args ...interface{}) func (c *B) Errorf(format string, args ...interface{}) func (c *B) Fail() func (c *B) FailNow() func (c *B) Failed() bool func (c *B) Fatal(args ...interface{}) func (c *B) Fatalf(format string, args ...interface{}) func (c *B) Log(args ...interface{}) func (c *B) Logf(format string, args ...interface{}) func (c *B) Name() string func (b *B) ReportAllocs() func (b *B) ResetTimer() func (b *B) Run(name string, f func(b *B)) bool func (b *B) RunParallel(body func(*PB)) func (b *B) SetBytes(n int64) func (b *B) SetParallelism(p int) func (c *B) Skip(args ...interface{}) func (c *B) SkipNow() func (c *B) Skipf(format string, args ...interface{}) func (c *B) Skipped() bool func (b *B) StartTimer() func (b *B) StopTimer()

Benchmark example

We write a benchmark for the split function in the split package as follows:

func BenchmarkSplit(b *testing.B) { for i := 0; i < b.N; i++ { Split("Sand river has sand and river", "sand") } }

Benchmark will not be executed by default, and the - bench parameter needs to be added, so we execute benchmark by executing the go test -bench=Split command, and the output result is as follows:

split $ go test -bench=Split goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 203 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.255s

Among them, the benchmark Split-8 represents benchmark of Split function, and the number 8 represents the value of GOMAXPROCS, which is very important for concurrent benchmark. 10000000 and 203ns/op indicate that the Split function takes 203ns per call, which is the average of 10000000 calls.

We can also add the - benchmem parameter to the benchmark to get the memory allocation statistics.

split $ go test -bench=Split -benchmem goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 215 ns/op 112 B/op 3 allocs/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.394s

Among them, 112 B/op indicates 112 bytes of memory are allocated for each operation, and 3 allocs/op indicates 3 times of memory allocation for each operation. We optimize our Split function as follows:

func Split(s, sep string) (result []string) { result = make([]string, 0, strings.Count(s, sep)+1) i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+len(sep):] // Here, use len(sep) to get the length of sep i = strings.Index(s, sep) } result = append(result, s) return }

This time, we use the make function to initialize the result into a slice with enough capacity in advance, instead of appending by calling the append function as before. Let's see how much performance improvement this improvement will bring:

split $ go test -bench=Split -benchmem goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 127 ns/op 48 B/op 1 allocs/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 1.423s

This change that uses the make function to allocate memory in advance reduces memory allocation times by 2 / 3 and memory allocation by half.

Performance comparison function

The above benchmark can only get the absolute time-consuming of a given operation, but in many performance problems, the relative time-consuming occurs between two different operations. For example, what is the difference between the time-consuming of the same function processing 1000 elements and the time-consuming of processing 10000 or even 1 million elements? Or which algorithm is the best for the same task? We usually need to use the same input to benchmark the implementation of two different algorithms.

Performance comparison function is usually a function with parameters, which is called by several different Benchmark functions with different values. For example:

func benchmark(b *testing.B, size int){/* ... */} func Benchmark10(b *testing.B){ benchmark(b, 10) } func Benchmark100(b *testing.B){ benchmark(b, 100) } func Benchmark1000(b *testing.B){ benchmark(b, 1000) }

For example, we have compiled a function to calculate Fibonacci sequence as follows:

// fib.go // Fib is a function to calculate the nth Fibonacci number func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-2) }

Our performance comparison functions are as follows:

// fib_test.go func benchmarkFib(b *testing.B, n int) { for i := 0; i < b.N; i++ { Fib(n) } } func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) } func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) } func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) } func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) } func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) } func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }

Run benchmark:

split $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib BenchmarkFib1-8 1000000000 2.03 ns/op BenchmarkFib2-8 300000000 5.39 ns/op BenchmarkFib3-8 200000000 9.71 ns/op BenchmarkFib10-8 5000000 325 ns/op BenchmarkFib20-8 30000 42460 ns/op BenchmarkFib40-8 2 638524980 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/fib 12.944s

Note here that by default, each Benchmark runs for at least one second. If the return time of the Benchmark function is less than 1 second, the value of b.N will be 1, 2, 5, 10, 20, 50 Increase, and the function runs again.

The final benchmark fib40 ran only twice, averaging less than a second each time. In this case, we should be able to use the - benchtime flag to increase the minimum reference time to produce more accurate results. For example:

split $ go test -bench=Fib40 -benchtime=20s goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib BenchmarkFib40-8 50 663205114 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/fib 33.849s

This time, the benchmark fib40 function runs 50 times, and the result will be more accurate.

When using the performance comparison function for testing, an easy mistake is to use b.N as the input size. For example, the following two examples are examples of mistakes:

// Error demonstration 1 func BenchmarkFibWrong(b *testing.B) { for n := 0; n < b.N; n++ { Fib(n) } } // Error demonstration 2 func BenchmarkFibWrong2(b *testing.B) { Fib(b.N) }

reset time

b. The processing before resettimer will not be put in the execution time or output to the report, so you can do some operations not planned as test report before. For example:

func BenchmarkSplit(b *testing.B) { time.Sleep(5 * time.Second) // Suppose you need to do something time-consuming and irrelevant b.ResetTimer() // Reset timer for i := 0; i < b.N; i++ { Split("Sand river has sand and river", "sand") } }

Parallel test

func (b *B) RunParallel(body func(*PB)) performs the given benchmark in parallel.

Runparallel will create multiple goroutines and assign b.N to these goroutines. The default value of the number of goroutines is GOMAXPROCS. If you want to increase the parallelism of the non cpu Limited (non-CPU-bound) benchmark, you can call SetParallelism before RunParallel. Runparallel is usually used with the - cpu flag.

func BenchmarkSplitParallel(b *testing.B) { // b.SetParallelism(1) / / sets the number of CPU s used b.RunParallel(func(pb *testing.PB) { for pb.Next() { Split("Sand river has sand and river", "sand") } }) }

Perform the following benchmark:

split $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 131 ns/op BenchmarkSplitParallel-8 50000000 36.1 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 3.308s

You can also specify the number of CPUs to use by adding a - CPU parameter such as go test -bench=. -cpu 1 after the test command.

Setup and TearDown

Test programs sometimes require additional setup before testing or teardown after testing.

TestMain

By defining the TestMain function in the * _test.go file, you can perform additional setup before the test or teardown after the test.

If the test file contains the function func TestMain(m *testing.M), the generated test will first call TestMain(m), and then run the specific test. TestMain runs in the main goroutine and can do any setup and teardown before and after m.Run is called. When you exit the test, you should call os.Exit with the return value of m.Run as a parameter.

An example of using TestMain to set up Setup and TearDown is as follows:

func TestMain(m *testing.M) { fmt.Println("write setup code here...") // Make some settings before testing // If TestMain uses flags, you should add flag.Parse() retCode := m.Run() // Execution testing fmt.Println("write teardown code here...") // Do some disassembly work after the test os.Exit(retCode) // Exit test }

Note that when calling TestMain, flag.Parse is not called. So if TestMain depends on the command-line flag (including the flag of the testing package), the call to flag.Parse should be displayed.

Setup and Teardown of subtests

Sometimes we may need to set up Setup and Teardown for each test set, or we may need to set up Setup and Teardown for each subtest. Here we define two function utility functions as follows:

// Test set Setup and Teardown func setupTestCase(t *testing.T) func(t *testing.T) { t.Log("Execute here if necessary:Before the test setup") return func(t *testing.T) { t.Log("Execute here if necessary:After the test teardown") } } // Setup and Teardown of subtests func setupSubTest(t *testing.T) func(t *testing.T) { t.Log("Execute here if necessary:Before subtest setup") return func(t *testing.T) { t.Log("Execute here if necessary:After the subtest teardown") } }

Use as follows:

func TestSplit(t *testing.T) { type test struct { // Define test structure input string sep string want []string } tests := map[string]test{ // Test cases using map storage "simple": }, "wrong sep": }, "more sep": }, "leading sep": }, } teardownTestCase := setupTestCase(t) // Perform setup before testing defer teardownTestCase(t) // Perform the testdoen operation after the test for name, tc := range tests { t.Run(name, func(t *testing.T) { // Use t.Run() to perform subtests teardownSubTest := setupSubTest(t) // Perform setup before subtest defer teardownSubTest(t) // Execute testdoen operation after test got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%#v, got:%#v", tc.want, got) } }) } }

The test results are as follows:

split $ go test -v === RUN TestSplit === RUN TestSplit/simple === RUN TestSplit/wrong_sep === RUN TestSplit/more_sep === RUN TestSplit/leading_sep --- PASS: TestSplit (0.00s) split_test.go:71: Execute here if necessary:Before the test setup --- PASS: TestSplit/simple (0.00s) split_test.go:79: Execute here if necessary:Before subtest setup split_test.go:81: Execute here if necessary:After the subtest teardown --- PASS: TestSplit/wrong_sep (0.00s) split_test.go:79: Execute here if necessary:Before subtest setup split_test.go:81: Execute here if necessary:After the subtest teardown --- PASS: TestSplit/more_sep (0.00s) split_test.go:79: Execute here if necessary:Before subtest setup split_test.go:81: Execute here if necessary:After the subtest teardown --- PASS: TestSplit/leading_sep (0.00s) split_test.go:79: Execute here if necessary:Before subtest setup split_test.go:81: Execute here if necessary:After the subtest teardown split_test.go:73: Execute here if necessary:After the test teardown === RUN ExampleSplit --- PASS: ExampleSplit (0.00s) PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
Example function

Format of sample function

The third function specially treated by go test is the Example function, whose function names are prefixed with Example. They have neither parameters nor return values. The standard format is as follows:

func ExampleName() { // ... }

Example function example

The following code is an example function we wrote for the Split function:

func ExampleSplit() { fmt.Println(split.Split("a:b:c", ":")) fmt.Println(split.Split("Sand river has sand and river", "sand")) // Output: // [a b c] // [there are rivers and rivers] }

There are three uses for writing sample code for your code:

  1. Example functions can be used directly as documents. For example, in web-based godoc, example functions can be associated with corresponding functions or packages.

  2. As long as the sample function contains / / Output: it is also an executable test that can be run through go test.

    split $ go test -run Example PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
  1. The sample function provides sample code that can be run directly. You can use Go Playground to run the sample code directly on the godoc document server of golang.org. The following figure shows the example function effect of strings.ToUpper function in Playground.

Exercises
  1. Write a palindrome detection function, write unit test and benchmark for it, and optimize it step by step according to the test results. (palindrome: the positive order and reverse order of a string are the same, such as "Madam,I'm Adam", "oil lamp with less oil", etc.)

Reprinted from Li Wenzhou blog

10 February 2020, 10:21 | Views: 2710

Add new comment

For adding a comment, please log in
or create account

0 comments