Unit testing in golang

Excellent code habits must be accompanied by unit testing, which is also the philosophy of go language design;

Many foreign companies and excellent programmers pay more attention to TDD, but it is very rare in China; (TDD: Test Driven Development)

In any case, learning and using golang unit tests is not a waste of time, but to make your code more elegant and robust!

Test file

File name in_ test. All files with go suffix are test code, which will be captured by go test test and will not be compiled by go build;

Test function

There are three types of functions in the test file:

  • Unit Test function: function name prefix Test; Logic of Test program
  • Benchmark function: function name prefix benchmark; Test function performance
  • Example function: function name prefix example; Will appear in godoc and provide sample documents for the documents

Test command

The test in Go language depends on the go test command; Add different parameters to achieve different test purposes; They will be introduced one by one later;

The go test command will traverse all the tests*_ test. Test functions in the go file that conform to the above naming rules;

Then a temporary main package is generated to call the corresponding test function, then build, run and report the test results, and finally clean up the temporary files generated in the test;

Next, unit test function, benchmark function and example function are introduced respectively:

Unit test function

  • Format of unit test function:

    1. func TestName(t *testing.T) {}
    2. Function names must start with Test, and optional suffixes must start with uppercase letters
    3. Each test function must import the testing package; About the methods in the testing package, you can take a look at the source code;
    4. The parameter t is used to report test failures and additional log information
  • A simple test function example: compare the output result with the expected result

    1. Create business function

      // File split / split Go: define a split package in which a split function is defined
      package split
      
      import "strings"
      
      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
      }
    2. Create test file

      // File split/split_test.go: create a split_test.go's test file
      package split
      
      import (
          "reflect"
          "testing"
      )
      
      // Unit test function
      // The Test function name must start with Test and must receive a * testing T type parameter
      // 1. Directly call business functions
      // 2. Define expected results
      // 3. Compare actual results with expected results
      func TestSplit(t *testing.T) {
          got := Split("a:b:c", ":")      // Call the program and return the program result
          want := []string{"a", "b", "c"} // Expected results
      
          if !reflect.DeepEqual(want, got) { // Because slice cannot be compared directly, it can be compared with the method in the reflection package
              t.Errorf("expected:%v, got:%v", want, got) // If the test fails, an error message is output
          }
      }
      
      // Provide a failed unit test
      func TestSplitFail(t *testing.T) {
          got := Split("abcd", "bc")
          want := []string{"a", "d"}
      
          if !reflect.DeepEqual(want, got) {
              t.Errorf("expected:%v, got:%v", want, got)
          }
      }
      
      // Benchmark function
      func BenchmarkSplit(b *testing.B) {
      
      }
      
      // Sample function
      func ExampleSplit() {
      
      }
    3. Execute test command

      Enter the split directory and directly run the go test command;

      If you run go test -v, you can see more detailed output results: you know which test function failed and where the fault is

      === RUN   TestSplit
      --- PASS: TestSplit (0.00s)
      === RUN   TestSplitFail
          split_test.go:28: expected:[a d], got:[a cd]
      --- FAIL: TestSplitFail (0.00s)
      FAIL
      exit status 1
      FAIL    gotest/split    0.001s
  • Other go test commands

    go test -run=?: Run corresponds to a regular expression. Only the test function whose function name matches will be executed by the go test command;

    // For example, the above code executes the command: go test -v -run=Fail
    // This means that only test functions that can regularly match Fail will be run this time
    === RUN   TestSplitFail
        split_test.go:28: expected:[a d], got:[a cd]
    --- FAIL: TestSplitFail (0.00s)
    FAIL
    exit status 1
    FAIL    gotest/split    0.001s

    go test -short: skip the test function including testing Test function of short() function; It is generally used to skip test functions that are too time-consuming to execute; For example:

    // Modify the TestSplitFail function in the above example code as follows
    func TestSplitFail(t *testing.T) {
        if testing.Short() {
            t.Skip("short The test case will be skipped in mode")
        }
        got := Split("abcd", "bc") // Call the program and return the program result
        want := []string{"a", "d"} // Expected results
    
        if !reflect.DeepEqual(want, got) { // Because slice cannot be compared directly, it can be compared with the method in the reflection package
            t.Errorf("expected:%v, got:%v", want, got) // If the test fails, an error message is output
        }
    }
    
    // Then execute the command 'go test -v -short' to print the following results:
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestSplitFail
        split_test.go:25: short The test case will be skipped in mode
    --- SKIP: TestSplitFail (0.00s)
    PASS
    ok      gotest/split    0.002s

    go test -cover test coverage: coverage refers to the proportion of business code covered by test code;

    go test -cover -coverprofile=c.out outputs the information related to coverage to the c.out file under the current folder;

    Then execute go tool cover -html=c.out and use the cover tool to process the generated record information. This command will open the local browser window and generate an HTML report;

  • Sub test: it can clearly and accurately locate the error of multiple groups of test cases

    Go1. A sub test is added in 7 +, and we can use t.Run to execute the sub test as follows:

    func TestSplit(t *testing.T) {
        type test struct {
            input string
            sep   string
            want  []string
        }
        // Define multiple sets of test cases
        tests := map[string]test{
            "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
            "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
            "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
            "leading sep": {input: "Sand river has sand and river", sep: "sand", want: []string{"Heyou", "Another river"}},
        }
    
        // t.Run is the subtest
        for name, tc := range tests {
            t.Run(name, func(t *testing.T) { // Use t.Run() to execute the subtest
                got := Split(tc.input, tc.sep)
                if !reflect.DeepEqual(got, tc.want) {
                    t.Errorf("expected:%#v, got:%#v", tc.want, got)
                }
            })
        }
    }
    
    // Execute go test -v
    === RUN   TestSplit
    === RUN   TestSplit/simple
    === RUN   TestSplit/wrong_sep
    === RUN   TestSplit/more_sep
        split_test.go:34: expected:[]string{"a", "d"}, got:[]string{"a", "cd"}
    === RUN   TestSplit/leading_sep
        split_test.go:34: expected:[]string{"Heyou", "Another river"}, got:[]string{"", "\xb2\x99 Heyou", "\xb2\x99 Another river"}
    --- FAIL: TestSplit (0.00s)
        --- PASS: TestSplit/simple (0.00s)
        --- PASS: TestSplit/wrong_sep (0.00s)
        --- FAIL: TestSplit/more_sep (0.00s)
        --- FAIL: TestSplit/leading_sep (0.00s)
    FAIL
    exit status 1
    FAIL    gotest/split    0.002s

Reference function

Test program performance under a certain workload;

Basic format: func BenchmarkName(b *testing.B) {}

  • Prefix with Benchmark, followed by capital letters
  • There must be testing Parameter of type B;
  • The benchmark test must be performed b.N times before it is comparable. The value of b.N is adjusted by the system according to the actual situation (see the following use example)
  • About testing B's method can refer to the source code
  • An example of a simple benchmark function:

    1. Create a test function (continue with the example code above)

      func BenchmarkSplit(b *testing.B) {
          // Attention b.N
          for i := 0; i < b.N; i++ {
              Split("Sand river has sand and river", "sand")
          }
      }
    2. Execute the test command - bench = $regular matching

      // go test -bench=Split
      goos: windows
      goarch: amd64
      pkg: go-test/split
      cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
      BenchmarkSplit-4         4017925               311.9 ns/op
      PASS
      ok      go-test/split      1.976s

      The above output some information of the computer;

      Where 4 in BenchmarkSplit-4 represents the value of GOMAXPROCS;

      4017925 indicates the number of times the function is called;

      311.9 ns/op average time spent calling this function;

      // `go test -bench=Split -benchmem ` increases the statistics of memory allocation
      goos: windows
      goarch: amd64
      pkg: go-test/split
      cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
      BenchmarkSplit-4         3995856               300.9 ns/op           112 B/op          3 allocs/op
      PASS
      ok      go-test/split      1.890s

      Where 112 B/op indicates that 112 bytes are allocated for each operation memory;

      3 allocs/op means that the memory is allocated three times for each operation;

  • Performance comparison function

    1. The benchmark function can only give absolute time consumption; In practice, we may want to know the relative time-consuming of different operations;
    2. The performance comparison function is usually a function with parameters, which is called by multiple different Benchmark functions with different values;
  • An example of a performance comparison function in a simple benchmark:

    1. Create a fib directory and create a new fib Go file

      package fib
      
      // Fib is a function that calculates the nth Fibonacci number
      func Fib(n int) int {
          if n < 2 {
              return n
          }
          return Fib(n-1) + Fib(n-2)
      }
    2. Create a test function file FIB in the same directory_ test. go

      package fib
      
      import (
          "testing"
      )
      
      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) }
    3. Execute test command

      // go test -bench=Fib
      goos: windows
      goarch: amd64
      pkg: go-test/fib
      cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
      BenchmarkFib1-4         470478618                2.393 ns/op
      BenchmarkFib2-4         178773399                6.737 ns/op
      BenchmarkFib3-4         100000000               12.60 ns/op
      BenchmarkFib10-4         3025942               421.8 ns/op
      BenchmarkFib20-4           24792             55344 ns/op
      BenchmarkFib40-4               2         724560000 ns/op
      PASS
      ok      go-interview/fib        11.675s

      Benchmarkfib40-4 2 724560000 NS / op: the first is the corresponding comparison function; The second is the number of execution; The third is the average execution time;

      It can be seen that the larger the input parameter value of fib function, the lower the efficiency of function execution;

  • In addition to the above, the benchmark command has other parameters for other purposes: for example, these parameters include benchtime, resettimer, runparallel, setparallelism, CPU, setup, teardown and testmain

Sample function

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

Adhere to the back-end related technologies such as daily output go development + interview questions + algorithm + work experience

For my plans for this year, please check: flag-2022

For more blog content, please check bigshake

Keywords: Go

Added by mook on Tue, 01 Mar 2022 06:22:28 +0200