[Go quality assurance: Unit Test & benchmark test]

Posted by murali on Tue, 04 Jan 2022 06:00:33 +0100

The current project development can never be completed by one person and requires multi person cooperation. In the process of multi person cooperation, how to ensure the quality of code and how to optimize the performance of code, let's take a look at unit testing and benchmarking.

unit testing

After the function is developed, if the code is directly merged into the code base for online use or used by others, there may be problems with the untested code logic. If the function is forcibly merged, it may affect the development of others. If it is forcibly online, it may cause online bugs and affect users' use.

What is unit testing

As the name suggests, unit testing emphasizes testing units. In development, a unit can be a function, a module, etc. Generally, the unit to be tested should be a complete minimum unit, such as the function of Go language. In this way, when each smallest unit is verified, the whole module and even the whole program can be verified. The unit test is written by the developer himself, that is, who has changed the code and who needs to write the corresponding unit test code to verify the correctness of this change.

Unit test of Go language

Although the concept of unit test is the same in each programming language, the design of unit test is different. Go language also has its own unit test specification.

go test -v ./test run this command to execute all unit tests under the test directory. If we see the PASS flag, it indicates that the unit test has passed, and we can also see the log written in the unit test. The Go language testing framework makes it easy for us to carry out unit testing, but we need to follow five rules:

1. With unit tests go File must have_test.go ending, Go Language testing tools only recognize files that meet this rule.
1. Unit test file name_test.go The first part is preferably where the function being tested is located go The file name of the file.
1. The function name of the unit test must be Test At the beginning, it is an exportable and public function.
1. The signature of the test function must receive a pointer to testing.T Type and cannot return any value.
1. The best function name is Test +  The name of the function to test.

Following the above rules, you can easily write unit tests. The focus of unit testing is to be familiar with the logic and scenarios of business code, so as to test as comprehensively as possible and ensure the code quality.

Unit test coverage

The Go language provides very convenient commands to view unit test coverage.

go test -v --covereprofile=test.cover ./test

// The above command contains the Flag -- coverprofile, which can get a unit test coverage file. Running this command can also see the test coverage at the same time.

// Output results
PASS
coverage:87% of statements
ok      xxx/test     0.27s     coverage:87% of statements


// Run the following command to get a unit test coverage report in HTML format:
go tool cover -html=test.cover -o=test.html

// After running, a test will be generated in the current directory HTML file, open it with a browser, and you can see the results.

The part marked in red is not tested, and the part marked in green is tested. This is the advantage of the unit test coverage report, which can easily detect whether the tests written by yourself are fully covered.

Benchmarking

In addition to ensuring that the logic of the code written is correct, there are sometimes performance requirements. How do you measure the performance of your code? This requires benchmarking.

What is benchmark? Benchmark is a method used to measure and evaluate software performance indicators, mainly used to evaluate the performance of code.

Go language benchmark

The benchmark and unit test rules of Go language are basically the same, but the naming rules of test functions are different.

func BenchmarkTest(b *testing.B){
    for i:=0; i<b.N; i++{
        // Code logic
    }
}

The benchmark example is as above. The difference between it and unit test is:

1. Benchmark functions must be **Benchmark** At the beginning, it must be exportable.
1. The signature of the function must receive one *testing.B Type and cannot return any value.
1. final for Loop is very important. The tested code should be put in the loop.
1. b.N It is provided by the benchmark framework and represents the number of cycles, because the tested code needs to be called repeatedly to evaluate the performance.

After the benchmark is written, you can test the performance through the following commands:

go test -bench=. ./test
goos: darwin
goarch: amd64
pkg : mytest/test
BenchmarkTest-8      3727612       343 ns/op
PASS
ok     mytest/test     2.120s

// To run the benchmark, you also need to use the go test command, but add the Flag - bench, which receives an expression as a parameter to match the benchmark function, "." Indicates that all benchmarks are run.

// In the output result, - 8 after the function indicates the value of GOMAXPROCS when running the benchmark. Next, 3727612 indicates the number of times to run the for loop, that is, the number of times to call the tested code. The last 343 ns/op indicates that it takes 343 nanoseconds each time.

// The default benchmark time is 1 second, that is, 3727612 calls in 1 second, each taking 343 nanoseconds. If you want to make the test run longer, you can specify it through - benchmark, such as 3 seconds:

go test -bench=. -benchmark=3s ./test


Running go test When using -benchmem this Flag Perform memory statistics through-benchmem The method of viewing memory is applicable to all benchmark test cases. The commands are as follows:
go test -bench=. -benchmem ./test

Timing method

Some preparations will be made before running the benchmark, such as building test data. These preparations consume time, so you need to exclude this part of time, so you need to reset the timer through the ResetTimer method. Examples are as follows:

func BenchmarkTest(b *testing.B) {
    n := 10
    b.ResetTimer() // Reset timer
    for i:= 0; i < b.N; i++ {
        // Code logic
    }
}

// This can avoid interference caused by the time-consuming preparation of data. In addition to the ResetTime method, there are also StartTimer and StopTimer methods to help us flexibly control when to start and stop timing.

Memory statistics

During the benchmark test, you can also count the number of memory allocated for each operation and the number of bytes allocated for each operation. These two indicators can be used as a reference for optimizing the code. It is also easy to start memory statistics. The code is as follows, that is, through the ReportAllocs() method:

func BenchmarkTest(b *testing.B) {
    n := 10
    b.ReportAllocs() // Turn on memory statistics
    b.ResetTimer() // Reset timer
    for i:=0; i <b.N; i++ {
        // Code logic
    }
}


go test -bench=. ./test
goos: darwin
goarch: amd64
pkg : mytest/test
BenchmarkTest-8      2317612       457 ns/op   0 B/op   0 allocs/op
PASS
ok     mytest/test     2.120s


// You can see that there are two more indicators than the original benchmark, namely 0 B/op and 0 allocs / Op. The former is how many bytes of memory are allocated for each operation, and the latter indicates the number of times memory is allocated for each operation. These two indicators can be used as a reference for code optimization. The smaller the better.

Tip: is the smaller the above two indicators, the better? This is not necessarily because sometimes you need space to change time, so it should be determined according to your specific business. The smaller the business is, the better.

Concurrent benchmarking

In addition to common benchmarks, Go language also supports concurrent benchmarks, which can test the performance of code under the concurrency of multiple goroutine s. The concurrent benchmark code is as follows:

func BenchmarkTestRunParallel (b *testing.B){
    n := 10
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // Code logic
        }
    })
}

// You can see that the Go language performs concurrency benchmarking through the RunParallel method. The RunParallel method creates multiple goroutines and assigns b.N to these goroutines for execution.

Benchmarking practice

Write a classic Fibonacci sequence. In recursive operation, there must be repeated calculation, which is the main factor affecting recursion. To solve the problem of repeated calculation, cache can be used. The calculated results can be reused.

// Cache calculated results
var cache= map[int]int{}

func Fibonacci(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 = Fibonacci(n-1) + Fiboncci(n-2)    
    }
    
    cache[n] = result
    return result
}

// The core of this set of code is to use a map to cache the calculated results for reuse. After the transformation, the code execution efficiency is very high and the performance is greatly improved.

BenchmarkTest-8   97823192    11.7 ns/op 

// It can be seen from the results that the performance is greatly improved compared with the previous performance of 11.7 nanoseconds.

Summary:

Unit testing is a good way to ensure code quality, but unit testing is not omnipotent. Using it can reduce the Bug rate, but don't rely entirely on it. In addition to unit testing, it can also assist Code Review, manual testing and other means to better ensure code quality.

Topics: Go unit testing