SoFunction
Updated on 2025-03-05

Tutorial for using Go language test library

Introduction

testingIt is a test library that comes with the Go language standard library. Writing tests in Go is simple, you only need to follow a few conventions of Go testing, which is no different from writing normal Go code. There are 3 types of tests in Go: unit test, performance test, and sample test. Let’s introduce it in sequence below.

Unit Testing

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 for converting strings and integers representing Roman numerals. Roman numerals are fromM/D/C/L/X/V/IThese 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 1-3999, cannot represent 0 and negative numbers, cannot represent integers 4000 or above, cannot represent fractions and decimals (of course, there are other complex rules to represent these numbers, which are not considered here for the time being);
  • Each integer has only one way to represent it. Generally speaking, the characters written in succession represent the corresponding integers added, for exampleI=1II=2III=3. However, the ten characters (I/X/C/M) appears at most 3 times, so it cannot be usedIIIIIndicates 4, it needs to beVAdd one to the leftI(Right nowIV) to indicate that it cannot be usedVIIIIIndicates 9, need to useIXreplace. Another five characters (V/L/D) Cannot appear twice in a row, so it cannot appearVV, need to useXreplace.
// 
package roman
import (
  "bytes"
  "errors"
  "regexp"
)
type romanNumPair struct {
  Roman string
  Num   int
}
var (
  romanNumParis []romanNumPair
  romanRegex    *
)
var (
  ErrOutOfRange   = ("out of range")
  ErrInvalidRoman = ("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 = (`^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 
  for _, pair := range romanNumParis {
    for n >  {
      ()
      n -= 
    }
  }
  return (), nil
}
func FromRoman(roman string) (int, error) {
  if !(roman) {
    return 0, ErrInvalidRoman
  }
  var result int
  var index int
  for _, pair := range romanNumParis {
    for roman[index:index+len()] ==  {
      result += 
      index += len()
    }
  }
  return result, nil
}

Writing a test in Go is simple, you just need to create a directory of the file where the function to be tested is located to_test.goThe ending file. In this file, we can write test functions one by one. The test function name must beTestXxxxThis form, andXxxxMust start with capital letters, and the function has a*Parameters of type:

// roman_test.go
package roman
import (
  "testing"
)
func TestToRoman(t *) {
  _, err1 := ToRoman(0)
  if err1 != ErrOutOfRange {
    ("ToRoman(0) expect error:%v got:%v", ErrOutOfRange, err1)
  }
  roman2, err2 := ToRoman(1)
  if err2 != nil {
    ("ToRoman(1) expect nil error, got:%v", err2)
  }
  if roman2 != "I" {
    ("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, determine whether the result is consistent with the expected, and call it if it is inconsistent.ofErrorf()Output error message. When running the test, these error messages will be collected and output uniformly after the run is completed.

After the test is completed, usego testThe command runs the test and outputs the result:

$ go test

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

I deliberatelyToRoman()A wrong line of code is written in the function.n > middle>It should be>=, the unit test successfully found the error. After modification, rerun the test:

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

All tests passed this time!

We can givego testCommand in-vOptions, output detailed test information:

$ go test -v

=== RUN   TestToRoman
--- PASS: TestToRoman (0.00s)
PASS
ok      /darjun/go-daily-lib/testing  0.174s

Before running each test function, output a line=== RUN, output after the run is finished--- PASSor--- FAILinformation.

Table-driven test

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

func TestToRoman(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()
    if got !=  {
      ("ToRoman(%d) expect:%s got:%s", , , got)
    }
    if err !=  {
      ("ToRoman(%d) expect error:%v got:%v", , , err)
    }
  }
}

The above lists each case to be tested and then calls it for each integerToRoman()Function, compares whether the returned Roman numeric string and the error value match the expected one. It is also very convenient to add new test cases in the future.

Grouping and paralleling

Sometimes testing with different dimensions of the same function is beneficial to maintenance. For example, aboveToRoman()The test of a function can be divided into three cases: illegal value, single Roman character and ordinary.

In order to group, I have done a certain degree of reconstruction of the code, first abstracting onetoRomanCasestructure:

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, "", },
    {4000, "", },
  }
  toRomanSingleCases = []toRomanCase{
    {1, "I", nil},
    {5, "V", nil},
    // ...
  }
  toRomanNormalCases = []toRomanCase{
    {2, "II", nil},
    {3, "III", nil},
    // ...
  }
}

Then to avoid duplication of code, abstract one and run multipletoRomanCaseFunction:

func testToRomanCases(cases []toRomanCase, t *) {
  for _, testCase := range cases {
    got, err := ()
    if got !=  {
      ("ToRoman(%d) expect:%s got:%s", , , got)
    }
    if err !=  {
      ("ToRoman(%d) expect error:%v got:%v", , , err)
    }
  }
}

Define a test function for each group:

func testToRomanInvalid(t *) {
  testToRomanCases(toRomanInvalidCases, t)
}
func testToRomanSingle(t *) {
  testToRomanCases(toRomanSingleCases, t)
}
func testToRomanNormal(t *) {
  testToRomanCases(toRomanNormalCases, t)
}

In the original test function, call()Run test functions of different groups,()The first parameter is the subtest name, and the second parameter is the subtest function:

func TestToRoman(t *) {
  ("Invalid", testToRomanInvalid)
  ("Single", testToRomanSingle)
  ("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      /darjun/go-daily-lib/testing  0.188s

You can see that 3 child tests are run in sequence, the child test name is the parent test name and()A combination of specified names, such asTestToRoman/Invalid

By default, these tests are executed sequentially. If there is no connection between the individual tests, we can have them in parallel to speed up the test. The method is also very simple,testToRomanInvalid/testToRomanSingle/testToRomanNormalThese 3 functions are called at the beginning(), because these 3 functions are called directlytestToRomanCases, or justtestToRomanCasesAdd at the beginning of the function:

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

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      /darjun/go-daily-lib/testing  0.182s

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

Also, in this example I willroman_test.goMoved the file toroman_testIn the package, so you needimport "/darjun/go-daily-lib/testing/roman". This method is very useful when the test package has circular dependencies, such as in the standard librarynet/httprelynet/urlurlTest function dependencynet/httpIf the test is placednet/urlIn the package, it will cause circular dependenciesurl_test(net/url)->net/http->net/url. You canurl_testPut it in a separate package.

Main test function

There is a special test function calledTestMain(), accept one*Parameters of type. This function is generally used to execute some initialization logic (such as creating a database link) before running all tests, or to execute some cleanup logic (release the database link) after all tests are run. If this function is defined in the test file,go testThe command will run this function directly, otherwisego testA default will be createdTestMain()function. The default behavior of this function is to run tests defined in the file. We customizeTestMain()When functioning, it also needs to be called manually()The method runs the test function, otherwise the test function will not run. DefaultTestMain()Similar to the following code:

func TestMain(m *) {
  (())
}

Customize one belowTestMain()Function, printgo testSupported options:

func TestMain(m *) {
  ()
  (func(f *) {
    ("name:%s usage:%s value:%v\n", , , )
  })
  (())
}

run:

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

These options can also be passedgo help testflagCheck.

other

Another functionFromRoman()I didn't write any tests, so I gave them to everyone 😀

Performance Testing

Performance testing is to evaluate the function's running performance. Performance testing must also be_test.goWrite in the file, and the function name must beBenchmarkXxxxbeginning. The performance test function accepts a*parameters. Next, we write 3 functions that calculate the nth Fibonacci number.

The first method: recursion

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

The second method: 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 method: 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 *) {
  for i := 0; i < ; i++ {
    Fib1(20)
  }
}
func BenchmarkFib2(b *) {
  for i := 0; i < ; i++ {
    Fib2(20)
  }
}
func BenchmarkFib3(b *) {
  for i := 0; i < ; i++ {
    Fib3(20)
  }
}

It is important to pay special attention toNgo testThis value will be adjusted until reliable performance data can be obtained during the test time. run:

$ go test -bench=.
goos: windows
goarch: amd64
pkg: /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      /darjun/go-daily-lib/testing/fib      5.225s

Performance tests will not be executed by default and need to be passed-bench=.Specify run.-benchThe value of the option is a simple pattern..means that all matchesFibIndicates that there is a running nameFibof.

The above test results indicateFib131110 times were executed within the specified time, with an average of 39144 ns per time,Fib2It ran 582637 times in the specified time, and the average time was 3127 ns per time.Fib3Run 191600582 times in the specified time, with an average of 5.588 ns per time.

Other options

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

-benchtime: Set the run time for each test.

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

Running for longer:

$ go test -bench=. -benchtime=30s
goos: windows
goarch: amd64
pkg: /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      /darjun/go-daily-lib/testing/fib      113.498s

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

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

-cpuprofile file: Write CPU sampled data to a file for easy usego tool pprofTool analysis, please see another article in my details"Go pprof you don't know"

run:

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

The CPU sampled data and memory allocation data are generated at the same time, throughgo tool pprofanalyze:

$ go tool pprof ./
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%  /darjun/fib.Fib1
    6490ms 17.71% 49.40%     6680ms 18.23%  /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%  /darjun/
    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%  /darjun/fib.BenchmarkFib3
(pprof)

Memory:

$ go tool pprof ./
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%  /darjun/
         0     0%   100%     8.69GB   100%  /darjun/fib.BenchmarkFib2
         0     0%   100%     8.69GB   100%  /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 in the file_test.goand the example test function name must beExampleXxxform. existExample*Write code in the function, and then write the expected output in the comments,go testThe function will be run and the actual output will be compared with the expected one. The following is extracted from Go source codenet/url/example_test.goThe code in the file demonstratesUsage:

func ExampleValuesGet() {
  v := {}
  ("name", "Ava")
  ("friend", "Jess")
  ("friend", "Sarah")
  ("friend", "Zoe")
  (("name"))
  (("friend"))
  (v["friend"])
  // Output:
  // Ava
  // Jess
  // [Jess Sarah Zoe]
}

CommentsOutput:Then there is the expected output result.go testThese functions will be run and compared to the expected result, and the comparison will ignore spaces.

Sometimes the order of output is uncertain, so we need to use itUnordered Output. We knowThe underlying type ismap[string][]string, so you can traverse the output of all key values, but the output order is uncertain:

func ExampleValuesAll() {
  v := {}
  ("name", "Ava")
  ("friend", "Jess")
  ("friend", "Sarah")
  ("friend", "Zoe")
  for key, values := range v {
    (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      /darjun/url   0.172s

No comments, or no commentsOutput/Unordered OutputThe function 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 reconstructions safer and more reassuring, unit testing is essential. Troubleshoot performance problems in the program, performance testing can come in handy. Sample testing is mainly used to demonstrate how to use a certain feature.

refer to

  • Testing Official Document:/pkg/testing/
  • Go daily library GitHub:/darjun/go-daily-lib

The above is the detailed content of the Go language test library testing tutorial. For more information about Go language test library testing, please pay attention to my other related articles!