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 useIIII
represents 4, needV
add a leftI
(ieIV
) to represent, can notVIIII
represent 9, requires the use ofIX
instead. Further five characters (V/L/D
) 2 can not be consecutive times, it can not appearVV
, needX
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
- Testing official document: https://golang.google.cn/pkg/testing/
- 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~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。