Introduction to the

Testing is the testing library that comes with the Go standard library. Writing tests in the Go language is as simple as following a few conventions for Go tests, no different from writing normal Go code. There are three types of tests in the Go language: unit tests, performance tests, and sample tests. The following are introduced in turn.

Unit testing

Unit tests, also known as functional tests, test the logic of functions, modules, and so on. Next, let’s write a library that translates strings representing Roman numerals to integers. Roman numerals are the characters M/D/C/L/X/V/I combined according to certain rules to represent a positive integer:

  • M=1000, D=500, C=100, L=50, X=10, V=5, I=1;
  • Only integers ranging from 1 to 3999, not zeros and negative numbers, not integers 4000 and above, not fractions and decimals (there are other complicated rules for representing these numbers, but we won’t consider them here);
  • Each integer can only be represented in one way. In general, concatenated characters represent the addition of corresponding integers, for exampleI=1.II=2.III=3. However,Ten characters(I/X/C/M) can not be used for a maximum of 3 timesIIIIThat means 4, and it has to be inVAdd one on the leftI(i.e.IV“, “, “, “, “,VIIIIIt’s 9, and you need to use itIXInstead. In additionFive characters(V/L/D) cannot appear twice in a row, so cannot appearVV, need to useXInstead.
// 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(a) {
  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({0, 3} ` ^ M (CM | | D CD? C {0, 3}) (XC | XL | L? X {0, 3}) (IX | | V IV? 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
}
Copy the code

Writing tests in Go is as simple as creating a file ending in _test.go in the same directory as the functionality to be tested. In this file, we can write each test function. The test function name must be of the form TestXxxx, which must start with a capital letter, and the function takes an argument of type *testing.T:

// roman_test.go
package roman

import (
  "testing"
)

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

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

The code written in the test function is no different than normal code, calls the corresponding function, returns the result, determines if the result is as expected, and if not, calls Errorf() in test.t to output an error message. When the test is run, the error information is collected and output uniformly after the test is run.

After the test is written, run the test using the go test command and output the results:

$ 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
Copy the code

I intentionally miswrote a line of code in the ToRoman() function, where > should be >= in n > pair.num, and the unit test successfully found the error. Rerun the test after the modification:

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

All passed this time!

We can also pass the -v option to the go test command to print the detailed test information:

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

Before each test function is RUN, the line === RUN is printed, and after the RUN is finished, the PASS or FAIL information is printed.

Table driven testing

In the example above, we actually only tested two cases, 0 and 1. It would be too tedious to write out each case in this way, and the popular way in Go is to use tables to list the data and results of each test:

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)
    ifgot ! = testCase.expect { t.Errorf("ToRoman(%d) expect:%s got:%s", testCase.num, testCase.expect, got)
    }

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

Each of the cases to be tested above is listed, and then the ToRoman() function is called for each integer to compare whether the returned Roman numeral string and error value match the expected value. It is also convenient to add new test cases later.

Grouping and parallelism

Sometimes there are tests of different dimensions to the same function, and combining these together is good for maintenance. For example, the above test for ToRoman() can be divided into three cases: invalid values, single Roman characters, and normal.

To group, I refactored the code to some extent, first abstracted a toRomanCase structure:

type toRomanCase struct {
  num    int
  expect string
  err    error
}
Copy the code

Divide all test data into 3 groups:

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

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

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

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

Then, to avoid code duplication, abstract a function that runs multiple Toromancases:

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

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

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)
}
Copy the code

In the original test function, call t.run () to run a different group of test functions. T.run () takes the subtest name in the first argument and the subtest function in the second argument:

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

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
Copy the code

As you can see, three child tests are run in turn, and the child test name is a combination of the parent test name and the name specified by t.run (), such as TestToRoman/Invalid.

By default, these tests are executed sequentially. If the tests are not related to each other, we can run them in parallel to speed up testing. Method is simple, in testToRomanInvalid/testToRomanSingle/testToRomanNormal the three function calls t.P beginning arallel (), because the three functions directly call testToRomanCases, You can also add only at the beginning of the testToRomanCases function:

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

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
Copy the code

We found that the tests were completed in a different order than we had specified.

Also, in this example I moved the roman_test.go file into the roman_test package, so I need to import “github.com/darjun/go-daily-lib/testing/roman”. This is useful if the test package has circular dependencies, such as net/ HTTP in the standard library, which depends on NET/URL, and the URL’s test function depends on NET/HTTP. If you put the test in the net/ URL package, This results in a circular dependency on url_test(net/ URL)->net/ HTTP ->net/ URL. You can put url_test in a separate package.

Main test function

There is a special test function called TestMain() that takes an argument of type * testing.m. This function is typically used to perform some initialization logic (such as creating a database link) before all tests are run, or some cleanup logic (releasing a database link) after all tests are run. If the function is defined in the test file, the go test command runs the function directly. If not, go test creates a default TestMain() function. The default behavior of this function is to run the tests defined in the file. When 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() looks like this:

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

Here’s a custom TestMain() function that prints the options supported by go Test:

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())
}
Copy the code

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
Copy the code

These options can also be viewed by going help testflag.

other

Another functionFromRoman()I didn’t write any tests, so I handed them over to you at 😀

The performance test

Performance tests are used to measure the performance of a function. Performance tests must also be written in the _test.go file, and the function name must start with BenchmarkXxxx. The performance test function takes a parameter of *testing.B. Let’s write three functions that calculate the NTH Fibonacci number.

The first way: recursion

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

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(n2 -, m) + fibHelper(n- 1, m)
  m[n] = v
  return v
}

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

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
}
Copy the code

Let’s test the efficiency of these three 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)}}Copy the code

Special attention should be paid to N, which will be adjusted by go Test until reliable performance data can be obtained by test time. 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
Copy the code

Performance tests are not executed by default and need to be run via -bench=. The value of the -bench option is a simple pattern, where. Means matches all, and Fib means runs with Fib in the name.

The above test results indicate that Fib1 is executed for 31110 times in the specified time, with an average of 39144 NS each time; Fib2 is executed for 582637 times in the specified time, with an average of 3127 ns each time; Fib3 is executed for 191600582 times in the specified time. The average time is 5.588 ns.

The other options

There are several options to control the execution of performance tests.

-benchtime: sets the running time of each test.

$ go test -bench=. -benchtime=30s
Copy the code

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
Copy the code

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

-memprofile file: writes memory allocation data to a file.

-CPUProfile File: Write the CPU sample data into a file for easy analysis using the Go Tool PProf tool. See my other article, “What you Don’t know about Go pProf”.

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
Copy the code

CPU sampling data and memory allocation data are generated at the same time, and analyzed by go Tool PProf:

$ 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)
Copy the code

Memory:

$ 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)
Copy the code

Sample test

Sample tests are used to demonstrate the use of a module or function. Similarly, the sample tests are written in the file _test.go, and the sample test function name must be of the form ExampleXxx. Write the code in the Example* function, and then write the expected output in the comment. The Go test runs the function and compares the actual output to the expected output. The following code from the Go source net/ URL /example_test. Go file demonstrates the use of url.values:

func ExampleValuesGet(a) {
  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]
}
Copy the code

The Output: comment is followed by the expected Output. Go Test runs these functions and compares them with the expected results, ignoring whitespace in the comparison.

Sometimes the order of Output is uncertain, so Unordered Output should be used. We know that url.values has the underlying type map[string][]string, so we can iterate over all key Values, but the order of output is not certain:

func ExampleValuesAll(a) {
  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]
}
Copy the code

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
Copy the code

Functions that have no comments or comments with no Output/Unordered Output are ignored.

conclusion

This article describes three kinds of tests in Go: unit tests, performance tests, and sample tests. Unit testing is essential to make your program more reliable and secure for future refactoring. Performance testing can be very useful in identifying performance problems in your application. Example tests are designed to demonstrate how to use a feature.

If you find a fun and useful Go library, please Go to GitHub and submit the issue😄

reference

  1. Testing the official document: golang. Google. Cn/PKG/testing…
  2. Go daily GitHub: github.com/darjun/go-d…

I

My blog: darjun.github. IO

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