This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!

Unit testing

As the name suggests, unit testing is testing a unit, which can be a function, a module, and so on. The unit of general testing should be a complete minimum unit, such as a function. So when each of the smallest units is validated, then the entire module can be validated. The Go language has its own unit testing specification, which we use as an example the Fibonacci sequence. Fibonacci: its 0th term is 0; The first term is 1; Starting with the second term, each term is equal to the sum of the first two terms. So its sequence is: 0, 1, 1, 2, 3, 5, 8, 13, 21… According to the above rule, the function can be summarized as:

F(0) =0
F(1) =1
F(2)=F(2 - 1)+F(2 - 2)
F(n)=F(n - 1)+F(n - 2)
Copy the code

Implementation function:

func F(n int) int {
	if n < 0 {
		return 0
	}
	if n == 0 {
		return 0
	}
	if n == 1 {
		return 1
	}
	return F(n- 1) + F(n2 -)}Copy the code

The Fibonacci sequence is calculated by recursive method. You need to create a new go file to store the unit test code. For example, the function you just wrote should be in test/main.go, and the test code should be in test/main_test.go.

func TestF(t *testing.T) {
	// Use a predefined set of Fibonacci numbers as a test case
	fsMap := map[int]int{}
	fsMap[0] = 0
	fsMap[1] = 1
	fsMap[2] = 1
	fsMap[3] = 2
	fsMap[4] = 3
	fsMap[5] = 5
	fsMap[6] = 8
	fsMap[7] = 13
	fsMap[8] = 21
	fsMap[9] = 34
	for k, v := range fsMap {
		fib := F(k)
		if v == fib {
			t.Logf("Correct :n = %d, value %d", k, fib)
		} else {
			t.Errorf("Error result: expected %d, but calculated value %d", v, fib)
		}
	}
}
Copy the code

In the test example, a set of test cases are predefined by map, and the results are calculated by F function. If the results are equal, the calculation of F function is correct; otherwise, the calculation is incorrect.

Unit test command

go test -v ./test

Here are some common parameters:

  • -bench regexp performs corresponding benchmarks, such as -bench=.
  • -cover Opens the test coverage;
  • -run regexp only runs functions that match regexp. For example, -run=Array then executes functions that start with Array;
  • -v Displays detailed test commands.

Run all unit tests in the test directory. Here we have only one unit test. Run the following command:

$ go test -v ./test
=== RUN   TestF
    sum_test.go:23: Correct :n is0And has a value of0
    sum_test.go:23: Correct :n is2And has a value of1
    sum_test.go:23: Correct :n is4And has a value of3
    sum_test.go:23: Correct :n is5And has a value of5
    sum_test.go:23: Correct :n is6And has a value of8
    sum_test.go:23: Correct :n is7And has a value of13
    sum_test.go:23: Correct :n is1And has a value of1
    sum_test.go:23: Correct :n is3And has a value of2
    sum_test.go:23: Correct :n is8And has a value of21
    sum_test.go:23: Correct :n is9And has a value of34
--- PASS: TestF (0.00s)
PASS
ok      project/test 0.585s
Copy the code

When you run the result, you can see the PASS flag, indicating that the unit test passed, and you can see the log that was written in the unit test. Unit testing is done under the testing framework provided by the Go language and follows five rules:

  1. The go file for the unit test must be_test.goIn the end, the Go language testing tool recognizes only files that meet this rule.
  2. The preceding section of the unit test filename _test.go is ideally the filename of the GO file where the function being tested resides.
  3. The function name of a unit Test must start with Test and be an exportable, public function.
  4. The signature of the test function must receive a pointer to type Testing.T and must not return any value.
  5. The function name should be Test + the name of the function being tested.

Unit test log

A test case may be executed concurrently, and using the log output provided by Testing.T ensures that the log is printed along with the test context. T provides several log output methods, such as:

The method of paraphrase
Log Logs are displayed and the test ends
Logf Logs are formatted and the test ends
Error Print the error log and end the test
Errorf Format the error log and end the test
Fatal Prints the fatal log and ends the test
Fatalf Format the fatal log and end the test

Unit test coverage

Has the F function in the above example been fully tested? We can use the command go test -v –coverprofile=test.cover./test

$ go test -v --coverprofile=test.cover ./test
=== RUN   TestF
    sum_test.go:23: Correct :n is9And has a value of34
    sum_test.go:23: Correct :n is0And has a value of0
    sum_test.go:23: Correct :n is1And has a value of1
    sum_test.go:23: Correct :n is6And has a value of8
    sum_test.go:23: Correct :n is7And has a value of13
    sum_test.go:23: Correct :n is8And has a value of21
    sum_test.go:23: Correct :n is2And has a value of1
    sum_test.go:23: Correct :n is3And has a value of2
    sum_test.go:23: Correct :n is4And has a value of3
    sum_test.go:23: Correct :n is5And has a value of5
--- PASS: TestF (0.00s)
PASS
coverage: 85.7% of statements
ok      project/test 0.521s  coverage: 85.7% of statements
Copy the code

As you can see, the test coverage is 85.7%, indicating that the F function has not been fully tested. Let’s take a look at the detailed unit test coverage report: Go tool cover -html=test.cover -o=test.html go tool cover -html=test.cover -o=test.html

The red marked part is not tested, and the green marked part is already tested. The unit test coverage report makes it easy to detect whether the unit tests are fully covered. According to the report, modify the unit test code to cover the code logic that is not covered:

fsMap[- 1] = 0
Copy the code

Run the unit test again and look at its unit test coverage, and you’ll see that it’s already 100%.

The benchmark

Benchmarking tests the performance and CPU consumption of a program. The rules for benchmarking are basically the same as for unit testing, except that the naming rules for the test functions are different. For example F() above, the benchmark code is:

func BenchmarkF(b *testing.B) {
    for i:=0; i<b.N; i++{ F(10)}}Copy the code

The difference between benchmarking and unit testing:

  1. The Benchmark function must start with Benchmark and must be exportable;
  2. The function signature must receive a pointer to type Testing.B and must not return any value;
  3. The code being tested is put in a for loop;
  4. B.N is provided by the benchmarking framework and represents the number of cycles because the tested code needs to be called repeatedly to evaluate performance.

To run a benchmark, you also use the go test command with the -bench Flag, which takes an expression as a parameter, “.” to run all the benchmarks. Go test-bench =… /test

goos: windows
goarch: amd64
pkg: project/test
cpu: Intel(R) Core(TM) i7- 10750.H CPU @ 2.60GHz
BenchmarkF- 12            4042413               280.1 ns/op
PASS
ok      project/test 2.208s
Copy the code

Result analysis:

  • The “-12” after the function represents the corresponding GOMAXPROCS value when the benchmark is run.
  • 4042413 represents the number of times the for loop is run, which is the number of times the code under test is called.
  • 280.1 ns/ OP indicates that each time takes 280.1 nanoseconds.

The benchmark’s default time is 1 second, which is 4042,413 calls per second, with each call taking 280.1 nanoseconds. Go test-bench =. -benchtime=5./test

Timing methods

  1. The ResetTimer method resets the timer
  2. The StartTimer method controls the start time
  3. The StopTimer method controls stopping the timing

The preparation that may occur before benchmarking, such as building test data, also takes time, so you need to exclude that time. In this case, we can use the ResetTimer method to reset the timer so that the test data is not disturbed by the preparation time:

func BenchmarkF(b *testing.B) {
   n := 20
   b.ResetTimer() // Reset the timer
   for i := 0; i < b.N; i++ {
      F(n)
   }
}
Copy the code

Memory statistics

When benchmarking, you can also count the number of times and bytes allocated per operation, which can be used as a reference for optimizing your code. To enable memory statistics, use ReportAllocs() :

func BenchmarkF(b *testing.B) {
	n := 20
	b.ReportAllocs() // Enable memory statistics
	b.ResetTimer() // Reset the timer
	for i := 0; i < b.N; i++ {
		F(n)
	}
}
Copy the code

Running result:

goos: windows
goarch: amd64
pkg: project/test
cpu: Intel(R) Core(TM) i7- 10750.H CPU @ 2.60GHz
BenchmarkF- 12    166467    35043 ns/op    0 B/op   0 allocs/op
PASS
ok      project/test 6.724s
Copy the code

There are two more indicators in the results than in the previous benchmark:

  1. 0 B/ OP, how many bytes of memory are allocated for each operation.
  2. 0 ALlocs /op: indicates the number of memory allocations for each operation.

Concurrency benchmark

In addition to the benchmarks described above, the Go language also supports concurrency benchmarks to test the performance of code under multiple Gorouting concurrency. The concurrency benchmark is tested through the RunParallel method, which creates multiple Goroutines and assigns b.N to them for execution:

func BenchmarkFibonacciRunParallel(b *testing.B) {
	n := 10
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			F(n)
		}
	})
}
Copy the code

F function optimization

The Fibonacci function F that we wrote above, because we use recursion, there must be double counting, which is the main factor affecting recursion. To solve the problem of double counting, we can use the cache, which saves the calculated results first, so as to reuse them later. The optimized function:

func F(n int) int {
	if v, ok := cache[n]; ok {
		return v
	}
	result := 0
	switch {
	case n < 0:
		result = 0
	case n == 0:
		result = 0
	case n == 1:
		result = 1
	default:
		result = F(n- 1) + F(n2 -)
	}
	cache[n] = result
	return result
}
Copy the code

Running result:

goos: windows
goarch: amd64
pkg: project/test
cpu: Intel(R) Core(TM) i7- 10750.H CPU @ 2.60GHz
BenchmarkFibonacciRunParallel- 12    563681175   2.123 ns/op
PASS
ok      project/test 2.006s
Copy the code

As you can see, the result is 2.123 nanoseconds, which is a big performance improvement over the previous optimization of 280.1 nanoseconds.