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/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 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 example
I=1
,II=2
,III=3
. However, the ten characters (I/X/C/M
) appears at most 3 times, so it cannot be usedIIII
Indicates 4, it needs to beV
Add one to the leftI
(Right nowIV
) to indicate that it cannot be usedVIIII
Indicates 9, need to useIX
replace. Another five characters (V/L/D
) Cannot appear twice in a row, so it cannot appearVV
, need to useX
replace.
// 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.go
The ending file. In this file, we can write test functions one by one. The test function name must beTestXxxx
This form, andXxxx
Must 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.of
Errorf()
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 test
The 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 test
Command in-v
Options, 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--- PASS
or--- FAIL
information.
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 onetoRomanCase
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, "", }, {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 multipletoRomanCase
Function:
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/testToRomanNormal
These 3 functions are called at the beginning()
, because these 3 functions are called directlytestToRomanCases
, or justtestToRomanCases
Add 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.go
Moved the file toroman_test
In 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/http
relynet/url
,url
Test function dependencynet/http
If the test is placednet/url
In the package, it will cause circular dependenciesurl_test(net/url)
->net/http
->net/url
. You canurl_test
Put 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 test
The command will run this function directly, otherwisego test
A 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 test
Supported 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 testflag
Check.
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.go
Write in the file, and the function name must beBenchmarkXxxx
beginning. 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 toN
,go test
This 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.-bench
The value of the option is a simple pattern..
means that all matchesFib
Indicates that there is a running nameFib
of.
The above test results indicateFib1
31110 times were executed within the specified time, with an average of 39144 ns per time,Fib2
It ran 582637 times in the specified time, and the average time was 3127 ns per time.Fib3
Run 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 pprof
Tool 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 pprof
analyze:
$ 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.go
and the example test function name must beExampleXxx
form. existExample*
Write code in the function, and then write the expected output in the comments,go test
The 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.go
The 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 test
These 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 is
map[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 Output
The 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!