3

Introduction

testing is the test library that comes with the Go language standard library. Writing tests in the Go language is very simple. You only need to follow the conventions Go tests, which is no different from writing normal Go code. There are 3 types of tests in the Go language: unit tests, performance tests, and example tests. Let's introduce them in turn.

unit test

Unit testing, also known as functional testing, is to test whether the logic of functions, modules, and other codes is correct. Next, we write a library to convert strings and integers representing Roman numerals to each other. Roman numerals are composed of M/D/C/L/X/V/I these characters are combined according to certain rules to represent a positive integer:

  • M=1000,D=500,C=100,L=50,X=10,V=5,I=1;
  • It can only represent integers in the range of 1-3999, cannot represent 0 and negative numbers, cannot represent integers of 4000 and above, cannot represent fractions and decimals (of course, there are other complicated rules to represent these numbers, which are not considered here);
  • There is only one way to represent each integer. In general, the II=2 I=1 , 0610a966a77d92, III=3 . However, ten characters ( I/X/C/M ) appears up to 3 times, so you can not use IIII represents 4, need V add a left I (ie IV ) to represent, can not VIIII represent 9, requires the use of IX instead. Further five characters ( V/L/D ) 2 can not be consecutive times, it can not appear VV , need X instead.
// roman.go
package roman

import (
  "bytes"
  "errors"
  "regexp"
)

type romanNumPair struct {
  Roman string
  Num   int
}

var (
  romanNumParis []romanNumPair
  romanRegex    *regexp.Regexp
)

var (
  ErrOutOfRange   = errors.New("out of range")
  ErrInvalidRoman = errors.New("invalid roman")
)

func init() {
  romanNumParis = []romanNumPair{
    {"M", 1000},
    {"CM", 900},
    {"D", 500},
    {"CD", 400},
    {"C", 100},
    {"XC", 90},
    {"L", 50},
    {"XL", 40},
    {"X", 10},
    {"IX", 9},
    {"V", 5},
    {"IV", 4},
    {"I", 1},
  }

  romanRegex = regexp.MustCompile(`^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$`)
}

func ToRoman(n int) (string, error) {
  if n <= 0 || n >= 4000 {
    return "", ErrOutOfRange
  }
  var buf bytes.Buffer
  for _, pair := range romanNumParis {
    for n > pair.Num {
      buf.WriteString(pair.Roman)
      n -= pair.Num
    }
  }

  return buf.String(), nil
}

func FromRoman(roman string) (int, error) {
  if !romanRegex.MatchString(roman) {
    return 0, ErrInvalidRoman
  }

  var result int
  var index int
  for _, pair := range romanNumParis {
    for roman[index:index+len(pair.Roman)] == pair.Roman {
      result += pair.Num
      index += len(pair.Roman)
    }
  }

  return result, nil
}

Writing tests in Go is very simple, just create a file ending _test.go in the same directory as the file where the function to be tested is located. In this file, we can write test functions one by one. The test function name must be of the form TestXxxx Xxxx must start with a capital letter, and the function has a parameter of type *testing.T

// roman_test.go
package roman

import (
  "testing"
)

func TestToRoman(t *testing.T) {
  _, err1 := ToRoman(0)
  if err1 != ErrOutOfRange {
    t.Errorf("ToRoman(0) expect error:%v got:%v", ErrOutOfRange, err1)
  }

  roman2, err2 := ToRoman(1)
  if err2 != nil {
    t.Errorf("ToRoman(1) expect nil error, got:%v", err2)
  }
  if roman2 != "I" {
    t.Errorf("ToRoman(1) expect:%s got:%s", "I", roman2)
  }
}

The code written in the test function is no different from the normal code. Call the corresponding function, return the result, and judge whether the result is consistent with the expectation. If it is inconsistent, call testing.T of Errorf() output error information. When running the test, these error messages will be collected and output uniformly after the run is over.

After the test is written, use the go test command to run the test and output the result:

$ go test
--- FAIL: TestToRoman (0.00s)
    roman_test.go:18: ToRoman(1) expect:I got:
FAIL
exit status 1
FAIL    github.com/darjun/go-daily-lib/testing  0.172s

I deliberately wrote a line of code in the ToRoman() n > pair.Num in > should be >= . The unit test successfully found the error. Re-run the test after modification:

$ go test
PASS
ok      github.com/darjun/go-daily-lib/testing  0.178s

This time the test passed!

We can also pass in the -v go test command to output detailed test information:

$ go test -v
=== RUN   TestToRoman
--- PASS: TestToRoman (0.00s)
PASS
ok      github.com/darjun/go-daily-lib/testing  0.174s

Before running each test function, output a line of === RUN --- PASS or --- FAIL after the end of the operation.

Form-driven testing

In the above example, we actually only tested two cases, 0 and 1. It is too cumbersome to write out each situation in this way. The form is popular in Go to list the test data and results:

func TestToRoman(t *testing.T) {
  testCases := []struct {
    num    int
    expect string
    err    error
  }{
    {0, "", ErrOutOfRange},
    {1, "I", nil},
    {2, "II", nil},
    {3, "III", nil},
    {4, "IV", nil},
    {5, "V", nil},
    {6, "VI", nil},
    {7, "VII", nil},
    {8, "VIII", nil},
    {9, "IX", nil},
    {10, "X", nil},
    {50, "L", nil},
    {100, "C", nil},
    {500, "D", nil},
    {1000, "M", nil},
    {31, "XXXI", nil},
    {148, "CXLVIII", nil},
    {294, "CCXCIV", nil},
    {312, "CCCXII", nil},
    {421, "CDXXI", nil},
    {528, "DXXVIII", nil},
    {621, "DCXXI", nil},
    {782, "DCCLXXXII", nil},
    {870, "DCCCLXX", nil},
    {941, "CMXLI", nil},
    {1043, "MXLIII", nil},
    {1110, "MCX", nil},
    {1226, "MCCXXVI", nil},
    {1301, "MCCCI", nil},
    {1485, "MCDLXXXV", nil},
    {1509, "MDIX", nil},
    {1607, "MDCVII", nil},
    {1754, "MDCCLIV", nil},
    {1832, "MDCCCXXXII", nil},
    {1993, "MCMXCIII", nil},
    {2074, "MMLXXIV", nil},
    {2152, "MMCLII", nil},
    {2212, "MMCCXII", nil},
    {2343, "MMCCCXLIII", nil},
    {2499, "MMCDXCIX", nil},
    {2574, "MMDLXXIV", nil},
    {2646, "MMDCXLVI", nil},
    {2723, "MMDCCXXIII", nil},
    {2892, "MMDCCCXCII", nil},
    {2975, "MMCMLXXV", nil},
    {3051, "MMMLI", nil},
    {3185, "MMMCLXXXV", nil},
    {3250, "MMMCCL", nil},
    {3313, "MMMCCCXIII", nil},
    {3408, "MMMCDVIII", nil},
    {3501, "MMMDI", nil},
    {3610, "MMMDCX", nil},
    {3743, "MMMDCCXLIII", nil},
    {3844, "MMMDCCCXLIV", nil},
    {3888, "MMMDCCCLXXXVIII", nil},
    {3940, "MMMCMXL", nil},
    {3999, "MMMCMXCIX", nil},
    {4000, "", ErrOutOfRange},
  }

  for _, testCase := range testCases {
    got, err := ToRoman(testCase.num)
    if got != testCase.expect {
      t.Errorf("ToRoman(%d) expect:%s got:%s", testCase.num, testCase.expect, got)
    }

    if err != testCase.err {
      t.Errorf("ToRoman(%d) expect error:%v got:%v", testCase.num, testCase.err, err)
    }
  }
}

Each case to be tested is enumerated above, and then the ToRoman() function is called for each integer, and the returned Roman numeral string and the error value are compared whether they are consistent with the expected. It is also very convenient to add new test cases later.

Grouping and paralleling

Sometimes there are tests of different dimensions for the same function, and combining these together is beneficial for maintenance. For example, the above ToRoman() can be divided into three cases: illegal value, single Roman character and ordinary.

In order to group, I refactored the code to a certain extent, first abstract a toRomanCase structure:

type toRomanCase struct {
  num    int
  expect string
  err    error
}

Divide all test data into 3 groups:

var (
  toRomanInvalidCases []toRomanCase
  toRomanSingleCases  []toRomanCase
  toRomanNormalCases  []toRomanCase
)

func init() {
  toRomanInvalidCases = []toRomanCase{
    {0, "", roman.ErrOutOfRange},
    {4000, "", roman.ErrOutOfRange},
  }

  toRomanSingleCases = []toRomanCase{
    {1, "I", nil},
    {5, "V", nil},
    // ...
  }

  toRomanNormalCases = []toRomanCase{
    {2, "II", nil},
    {3, "III", nil},
    // ...
  }
}

Then in order to avoid code duplication, abstract a function that toRomanCase

func testToRomanCases(cases []toRomanCase, t *testing.T) {
  for _, testCase := range cases {
    got, err := roman.ToRoman(testCase.num)
    if got != testCase.expect {
      t.Errorf("ToRoman(%d) expect:%s got:%s", testCase.num, testCase.expect, got)
    }

    if err != testCase.err {
      t.Errorf("ToRoman(%d) expect error:%v got:%v", testCase.num, testCase.err, err)
    }
  }
}

Define a test function for each group:

func testToRomanInvalid(t *testing.T) {
  testToRomanCases(toRomanInvalidCases, t)
}

func testToRomanSingle(t *testing.T) {
  testToRomanCases(toRomanSingleCases, t)
}

func testToRomanNormal(t *testing.T) {
  testToRomanCases(toRomanNormalCases, t)
}

In the original test function, call t.Run() run the test functions of different groups. The t.Run() is the sub-test name, and the second parameter is the sub-test function:

func TestToRoman(t *testing.T) {
  t.Run("Invalid", testToRomanInvalid)
  t.Run("Single", testToRomanSingle)
  t.Run("Normal", testToRomanNormal)
}

run:

$ go test -v
=== RUN   TestToRoman
=== RUN   TestToRoman/Invalid
=== RUN   TestToRoman/Single
=== RUN   TestToRoman/Normal
--- PASS: TestToRoman (0.00s)
    --- PASS: TestToRoman/Invalid (0.00s)
    --- PASS: TestToRoman/Single (0.00s)
    --- PASS: TestToRoman/Normal (0.00s)
PASS
ok      github.com/darjun/go-daily-lib/testing  0.188s

It can be seen in order to run three sub-tests, subtests name is the name of the Father and test t.Run() name specified combination, such as TestToRoman/Invalid .

By default, these tests are executed sequentially. If there is no connection between the various tests, we can make them parallel to speed up the test. The method is very simple, in testToRomanInvalid/testToRomanSingle/testToRomanNormal call at the beginning of these three functions t.Parallel() , since this function directly calls the three testToRomanCases , can only testToRomanCases the beginning of the function add:

func testToRomanCases(cases []toRomanCase, t *testing.T) {
  t.Parallel()
  // ...
}

run:

$ go test -v
...
--- PASS: TestToRoman (0.00s)
    --- PASS: TestToRoman/Invalid (0.00s)
    --- PASS: TestToRoman/Normal (0.00s)
    --- PASS: TestToRoman/Single (0.00s)
PASS
ok      github.com/darjun/go-daily-lib/testing  0.182s

We found that the order in which the tests were completed was not the order we specified.

In addition, in this example, I moved the roman_test.go file to the roman_test package, so import "github.com/darjun/go-daily-lib/testing/roman" is needed. In this manner the test pack has a circular dependency case is useful, for example, the standard library net/http dependent net/url , url test functions dependent net/http , if the test in net/url package, it will result in a circular dependency url_test(net/url) -> net/http - > net/url . At this time, you can put url_test in a separate bag.

Main test function

There is a special test function named TestMain() , which accepts a *testing.M type 0610a966a7849e. This function is generally used to run all tests perform some initialization logic before (such as creating a database link), or all tests perform some logic to clean up after the run ends (release database link). If this function is defined in the test file, the go test command will run this function directly, otherwise go test will create a default TestMain() function. The default behavior of this function is to run the test defined in the file. we customize the TestMain() function, we also need to manually call the m.Run() method to run the test function, otherwise the test function will not run . The default TestMain() similar to the following code:

func TestMain(m *testing.M) {
  os.Exit(m.Run())
}

Here is a TestMain() function go test print the options supported by 0610a966a784f4:

func TestMain(m *testing.M) {
  flag.Parse()
  flag.VisitAll(func(f *flag.Flag) {
    fmt.Printf("name:%s usage:%s value:%v\n", f.Name, f.Usage, f.Value)
  })
  os.Exit(m.Run())
}

run:

$ go test -v
name:test.bench usage:run only benchmarks matching `regexp` value:
name:test.benchmem usage:print memory allocations for benchmarks value:false
name:test.benchtime usage:run each benchmark for duration `d` value:1s
name:test.blockprofile usage:write a goroutine blocking profile to `file` value:
name:test.blockprofilerate usage:set blocking profile `rate` (see runtime.SetBlockProfileRate) value:1
name:test.count usage:run tests and benchmarks `n` times value:1
name:test.coverprofile usage:write a coverage profile to `file` value:
name:test.cpu usage:comma-separated `list` of cpu counts to run each test with value:
name:test.cpuprofile usage:write a cpu profile to `file` value:
name:test.failfast usage:do not start new tests after the first test failure value:false
name:test.list usage:list tests, examples, and benchmarks matching `regexp` then exit value:
name:test.memprofile usage:write an allocation profile to `file` value:
name:test.memprofilerate usage:set memory allocation profiling `rate` (see runtime.MemProfileRate) value:0
name:test.mutexprofile usage:write a mutex contention profile to the named file after execution value:
name:test.mutexprofilefraction usage:if >= 0, calls runtime.SetMutexProfileFraction() value:1
name:test.outputdir usage:write profiles to `dir` value:
name:test.paniconexit0 usage:panic on call to os.Exit(0) value:true
name:test.parallel usage:run at most `n` tests in parallel value:8
name:test.run usage:run only tests and examples matching `regexp` value:
name:test.short usage:run smaller test suite to save time value:false
name:test.testlogfile usage:write test action log to `file` (for use only by cmd/go) value:
name:test.timeout usage:panic test binary after duration `d` (default 0, timeout disabled) value:10m0s
name:test.trace usage:write an execution trace to `file` value:
name:test.v usage:verbose: print additional output value:tru

These options can also be viewed through go help testflag .

other

Another function FromRoman() I did not write any test, I will leave it to you 😀

Performance Testing

The performance test is to evaluate the running performance of the function. The performance test must also be _test.go file, and the function name must start with BenchmarkXxxx The performance test function accepts a parameter of *testing.B Below we write 3 functions to calculate the nth Fibonacci number.

The first way: recursion

func Fib1(n int) int {
  if n <= 1 {
    return n
  }
  
  return Fib1(n-1) + Fib1(n-2)
}

The second way: memo

func fibHelper(n int, m map[int]int) int {
  if n <= 1 {
    return n
  }

  if v, ok := m[n]; ok {
    return v
  }
  
  v := fibHelper(n-2, m) + fibHelper(n-1, m)
  m[n] = v
  return v
}

func Fib2(n int) int {
  m := make(map[int]int)
  return fibHelper(n, m)
}

The third way: iteration

func Fib3(n int) int {
  if n <= 1 {
    return n
  }
  
  f1, f2 := 0, 1
  for i := 2; i <= n; i++ {
    f1, f2 = f2, f1+f2
  }
  
  return f2
}

Let's test the execution efficiency of these 3 functions:

// fib_test.go
func BenchmarkFib1(b *testing.B) {
  for i := 0; i < b.N; i++ {
    Fib1(20)
  }
}

func BenchmarkFib2(b *testing.B) {
  for i := 0; i < b.N; i++ {
    Fib2(20)
  }
}

func BenchmarkFib3(b *testing.B) {
  for i := 0; i < b.N; i++ {
    Fib3(20)
  }
}

Special attention should be N , go test will always adjust this value until the test time can get reliable performance data. run:

$ go test -bench=.
goos: windows
goarch: amd64
pkg: github.com/darjun/go-daily-lib/testing/fib
cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
BenchmarkFib1-8            31110             39144 ns/op
BenchmarkFib2-8           582637              3127 ns/op
BenchmarkFib3-8         191600582            5.588 ns/op
PASS
ok      github.com/darjun/go-daily-lib/testing/fib      5.225s

The performance test will not be executed by default, and it needs to be run -bench=. -bench option is a simple pattern, . means match all, and Fib means that there is Fib in the running name.

The above test results indicate that Fib1 has been executed 31110 times within the specified time, with an average of 39144 ns each time, Fib2 has been executed 582637 times within the specified time, and the average time taken each time is 3127 ns, Fib3 has been executed 191600582 times within the specified time, on average Each time it takes 5.588 ns.

other options

There are some options to control the execution of the performance test.

-benchtime : Set the running time of each test.

$ go test -bench=. -benchtime=30s

Run longer:

$ go test -bench=. -benchtime=30s
goos: windows
goarch: amd64
pkg: github.com/darjun/go-daily-lib/testing/fib
cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
BenchmarkFib1-8           956464             38756 ns/op
BenchmarkFib2-8         17862495              2306 ns/op
BenchmarkFib3-8       1000000000             5.591 ns/op
PASS
ok      github.com/darjun/go-daily-lib/testing/fib      113.498s

-benchmem : Output the memory allocation of the performance test function.

-memprofile file : Write the memory allocation data to the file.

-cpuprofile file : Write the CPU sampling data to a file, which is convenient go tool pprof tool. For details, see my other article "Pprof of Go You Don’t Know"

run:

$ go test -bench=. -benchtime=10s -cpuprofile=./cpu.prof -memprofile=./mem.prof
goos: windows
goarch: amd64
pkg: github.com/darjun/fib
BenchmarkFib1-16          356006             33423 ns/op
BenchmarkFib2-16         8958194              1340 ns/op
BenchmarkFib3-16        1000000000               6.60 ns/op
PASS
ok      github.com/darjun/fib   33.321s

At the same time, CPU sampling data and memory allocation data are go tool pprof analyzed by 0610a966a7892b:

$ go tool pprof ./cpu.prof
Type: cpu
Time: Aug 4, 2021 at 10:21am (CST)
Duration: 32.48s, Total samples = 36.64s (112.81%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 29640ms, 80.90% of 36640ms total
Dropped 153 nodes (cum <= 183.20ms)
Showing top 10 nodes out of 74
      flat  flat%   sum%        cum   cum%
   11610ms 31.69% 31.69%    11620ms 31.71%  github.com/darjun/fib.Fib1
    6490ms 17.71% 49.40%     6680ms 18.23%  github.com/darjun/fib.Fib3
    2550ms  6.96% 56.36%     8740ms 23.85%  runtime.mapassign_fast64
    2050ms  5.59% 61.95%     2060ms  5.62%  runtime.stdcall2
    1620ms  4.42% 66.38%     2140ms  5.84%  runtime.mapaccess2_fast64
    1480ms  4.04% 70.41%    12350ms 33.71%  github.com/darjun/fib.fibHelper
    1480ms  4.04% 74.45%     2960ms  8.08%  runtime.evacuate_fast64
    1050ms  2.87% 77.32%     1050ms  2.87%  runtime.memhash64
     760ms  2.07% 79.39%      760ms  2.07%  runtime.stdcall7
     550ms  1.50% 80.90%     7230ms 19.73%  github.com/darjun/fib.BenchmarkFib3
(pprof)

RAM:

$ go tool pprof ./mem.prof
Type: alloc_space
Time: Aug 4, 2021 at 10:30am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 8.69GB, 100% of 8.69GB total
Dropped 12 nodes (cum <= 0.04GB)
      flat  flat%   sum%        cum   cum%
    8.69GB   100%   100%     8.69GB   100%  github.com/darjun/fib.fibHelper
         0     0%   100%     8.69GB   100%  github.com/darjun/fib.BenchmarkFib2
         0     0%   100%     8.69GB   100%  github.com/darjun/fib.Fib2 (inline)
         0     0%   100%     8.69GB   100%  testing.(*B).launch
         0     0%   100%     8.69GB   100%  testing.(*B).runN
(pprof)

Sample test

Sample tests are used to demonstrate the use of modules or functions. Similarly, the sample test is also _test.go , and the sample test function name must be in the form ExampleXxx Write the code in the Example* function, and then write the desired output in the comments, go test will run the function, and then compare the actual output with the expected output. The following code extracted from the Go source code net/url/example_test.go file demonstrates the usage url.Values

func ExampleValuesGet() {
  v := url.Values{}
  v.Set("name", "Ava")
  v.Add("friend", "Jess")
  v.Add("friend", "Sarah")
  v.Add("friend", "Zoe")
  fmt.Println(v.Get("name"))
  fmt.Println(v.Get("friend"))
  fmt.Println(v["friend"])
  // Output:
  // Ava
  // Jess
  // [Jess Sarah Zoe]
}

In the comments, Output: is the expected output result. go test will run these functions and compare with the expected result. The comparison will ignore spaces.

Sometimes the order of our output is uncertain, then we need to use Unordered Output . We know url.Values underlying type map[string][]string , so the output can traverse all of the keys, but the output sequence of uncertainty:

func ExampleValuesAll() {
  v := url.Values{}
  v.Set("name", "Ava")
  v.Add("friend", "Jess")
  v.Add("friend", "Sarah")
  v.Add("friend", "Zoe")
  for key, values := range v {
    fmt.Println(key, values)
  }
  // Unordered Output:
  // name [Ava]
  // friend [Jess Sarah Zoe]
}

run:

$ go test -v
$ go test -v
=== RUN   ExampleValuesGet
--- PASS: ExampleValuesGet (0.00s)
=== RUN   ExampleValuesAll
--- PASS: ExampleValuesAll (0.00s)
PASS
ok      github.com/darjun/url   0.172s

Functions without comments or without Output/Unordered Output in the comments will be ignored.

Summarize

This article introduces 3 types of tests in Go: unit tests, performance tests, and sample tests. In order to make the program more reliable and make future refactorings safer and more assured, unit testing is essential. To troubleshoot performance problems in the program, performance testing can come in handy. The sample test is mainly to demonstrate how to use a certain function.

If you find a fun and useful Go language library, welcome to submit an issue on the Go Daily Library GitHub😄

refer to

  1. Testing official document: https://golang.google.cn/pkg/testing/
  2. Go a library GitHub every day: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~


darjun
2.9k 声望358 粉丝