Go Test Series 1: Unit Testing Go Theme Month

Go Test Series 1: Unit Testing Go Theme Month

This article will take you to get started with Golang unit testing. I refer to different materials and manual demos to see how to write tests in the Golang world and warm up for Golang TDD.

Common libraries

testing //system comes with testing libraries (necessary) Copy the code
Go GET github.com/stretchr/testify //assertion library copy the code

testify
The library allows you to write test assertions that are more readable:

Simple example

Go language recommends that the test file and the source code file be placed together, and the test file ends with _test.go. For example, the current package has a calc.go file, and we want to test the Add and Mul functions in calc.go, then we should create a new calc_test.go as the test file.

example/ |--calc.go |--calc_test.go Copy code

calc.go

package main func Add (a int , b int ) int { return a + b } func Mul (a int , b int ) int { return a * b } Copy code

calc_test.go

package main import "testing" /** Cd to the unittest directory, and then run directly from the command line: go test If you want to display the detailed verification of each test method: go test -v If you want to specify to run only a certain test: go test -run TestAdd -v t.Fatal/t.Fatalf Stop when you make a mistake t.Error/t.Errorf keeps getting errors */ func TestAdd (t *testing.T) { if ans := Add( 1 , 2 ); ans != 3 { t.Errorf( "1 + 2 expected be 3, but %d got" , ans) } if ans := Add( -10 , -20 ); ans != -30 { t.Errorf( "-10 + -20 expected be -30, but %d got" , ans) } } func TestMul (t *testing.T) { if ans := Mul( 1 , 2 ); ans != 2 { t.Errorf( "1 + 2 expected be 2, but %d got" , ans) } if ans := Mul( -10 , -20 ); ans != 200 { t.Errorf( "-10 + -20 expected be 200, but %d got" , ans) } } Copy code

How to run the test?

Cd to the unittest directory, and then run directly from the command line:

go test
If you want to display the detailed verification of each test method:
go test -v
If you want to specify to run only a certain test:
go test -run TestAdd -v

In addition:

  • t.Fatal/t.Fatalf
    Stop when you make a mistake

  • t.Error/t.Errorf
    Keep making mistakes

For detailed commands, see here:

go test//Find the use case file in the current directory go test pkg//all examples in the pkg package go test helloworld_test.go//Specify the use case file go test -v -run TestA select_test.go//Specify a single unit of the file to run. '-v' print detailed information go test -v -bench=. benchmark_test.go//A certain performance case of the specified file is run. '.' means all performance use cases go test -v -bench=. -benchtime=5s benchmark_test.go//'-benchtime=5s' specifies the test duration, the default is 1s go test -v -bench=Alloc -benchmem benchmark_test.go//Specify a single performance use case go test -cover//coverage go test ./... -v -cover//Run all test files in the current directory and subdirectories, and display detailed information and display coverage Copy code

subtest

cal_test.go

..... func TestMul_SubTests (t *testing.T) { t.Run( "should_return_6_when_Mul_given_2_and_3" , func (t * testing.T) { if ans := Mul( 2 , 3 ); ans != 6 { t.Fatal( "fail" ) } }) t.Run( "should_return_negative_6_when_Mul_given_2_and_negative_3" , func (t * testing.T) { if ans := Mul( 2 , -3 ); ans != -6 { t.Fatal( "fail" ) } }) } Copy code

Key statement:

t.RunCopy code

table-driven tests

We even more respect this way of writing:

demo2.go

package main func MergeString (x, y string ) string { return x + y } Copy code

demo2_test.go

package main import "testing" func TestMergeString (t *testing.T) { tests := [] struct { name string X, Y, Expected string }{ { "should_return_HelloWorld_when_MergeString_given_Hello_and_World" , "Hello" , "World" , "HelloWorld" }, { "should_return_aaaBBB_when_MergeString_given_aaa_and_BBB" , "aaa" , "bbb" , "aaaBBB" }, } for _, test := range tests { t.Run(test.name, func (t *testing.T) { if ans:= MergeString(test.X, test.Y);ans!=test.Expected { t.Error( "fail" ) } }) } } Copy code

Data for all use cases is organized in slices

cases
In, it looks like a table, with the help of loops to create sub-tests. The advantages of writing like this are:

  • Adding a new use case is very simple, just add a piece of test data to the cases.

  • The test code is readable, and you can intuitively see the parameters of each subtest and the expected return value.

  • When the use case fails, the format of the error message is relatively uniform, and the test report is easy to read.

If the amount of data is large, or some binary data, it is recommended to use a relative path to read from the file

Helper function

demo3_test.go

package main import "testing" type myCase struct { Str string Expected string } //Helper function: used to reconstruct some common code func createFirstLetterToUpperCase (t *testing.T, c *myCase) { //to.Helper() is used to print out the error corresponding when running go test Line number t.Helper() if ans := FirstLetterToUpperCase(c.Str); ans != c.Expected { t.Errorf( "input is `%s`, expect output is `%s`, but actually output `%s`" , c.Str, c.Expected, ans) } } func TestFirstLetterToUpperCase (t *testing.T) { createFirstLetterToUpperCase(t, &myCase{ "hello" , "Hello" }) createFirstLetterToUpperCase(t, &myCase{ "ok" , "Ok" }) createFirstLetterToUpperCase(t, &myCase{ "Good" , "Good" }) createFirstLetterToUpperCase(t, &myCase{ "GOOD" , "Good" }) } Copy code

demo3.go

package main import "strings" func FirstLetterToUpperCase (x string ) string { return strings.ToUpper(x[: 1 ]) + x[ 1 :] } Copy code

Key syntax:

to.Helper () //for Running go test print are given line numbers corresponding to the duplicated code

on

helper
2 suggestions for functions:

  • Don't return an error, use it directly inside the help function

    t.Error
    or
    t.Fatal
    That is, the main logic of the use case will not affect the readability due to too many error handling codes.

  • transfer

    t.Helper()
    Make the error information more accurate and help positioning.

setup and teardown

In fact, it is some life cycle related functions of test

Setup can do some initialization operations, teardown can do some resource recovery work.

func setup () { fmt.Println( "Before all tests" ) } func teardown () { fmt.Println( "After all tests" ) } func Test1 (t *testing.T) { fmt.Println( "I'm test1" ) } func Test2 (t *testing.T) { fmt.Println( "I'm test2" ) } func TestMain (m *testing.M) { setup() code := m.Run() teardown() os.Exit(code) } Copy code

Description:

  • In this test file, there are 2 test cases,

    Test1
    with
    Test2
    .

  • If the test file contains functions

    TestMain
    , Then the generated test will call TestMain(m) instead of running the test directly.

  • transfer

    m.Run()
    Trigger the execution of all test cases and use
    os.Exit()
    The status code returned by the processing, if it is not 0, it indicates that the useful case has failed.

  • So you can call

    m.Run()
    Do some extra preparation (setup) and recycling (teardown) work before and after.

carried out

go test
, Will output:

$ go test Before all tests I'm test1 I'm test2 PASS After all tests ok example 0.006 s Copy code

Network test (Network)

TCP/HTTP

Suppose you need to test that the handler of an API interface can work normally, such as helloHandler

func helloHandler (w http.ResponseWriter, r *http.Request) { w.Write([] byte ( "hello world" )) } Copy code

Then we can create a real network connection for testing:

//test code import ( "io/ioutil" "net" "net/http" "testing" ) func handleError (t *testing.T, err error) { t.Helper() if err != nil { t.Fatal( "failed" , err) } } func TestConn (t *testing.T) { ln, err := net.Listen( "tcp" , "127.0.0.1:0" ) handleError(t, err) defer ln.Close() http.HandleFunc( "/hello" , helloHandler) go http.Serve(ln, nil ) resp, err := http.Get( "http://" + ln.Addr().String() + "/hello" ) handleError(t, err) defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) handleError(t, err) if string (body) != "hello world" { t.Fatal( "expected hello world, but got" , string (body)) } } Copy code
  • net.Listen("tcp", "127.0.0.1:0")
    : Monitor an unoccupied port and return to Listener.

  • transfer

    http.Serve(ln, nil)
    Start the http service.

  • use

    http.Get
    Initiate a Get request and check whether the return value is correct.

  • Try to be wrong

    http
    with
    net
    The library uses mocks, which can cover more realistic scenes.

httptest

For http development scenarios, it is more efficient to use the standard library net/http/httptest for testing.

The above test case is rewritten as follows:

//test code import ( "io/ioutil" "net/http" "net/http/httptest" "testing" ) func TestConn (t *testing.T) { req := httptest.NewRequest( "GET" , "http://example.com/foo" , nil ) w := httptest.NewRecorder() helloHandler(w, req) bytes, _ := ioutil.ReadAll(w.Result().Body) if string (bytes) != "hello world" { t.Fatal( "expected hello world, but got" , string (bytes)) } } Copy code

Use httptest to simulate the request object (req) and response object (w) to achieve the same goal.

Benchmark

The definition of the benchmark test case is as follows:

func BenchmarkName (b *testing.B) { //... } Copy code
  • The function name must start with

    Benchmark
    At the beginning, usually followed by the name of the function to be tested

  • The parameters are

    b *testing.B
    .

  • When performing benchmark tests, you need to add

    -bench
    parameter.

E.g:

func BenchmarkHello (b *testing.B) { for i := 0 ; i <bN; i++ { fmt.Sprintf( "hello" ) } } Copy code

run:

go test -benchmem -bench.
got the answer:

BenchmarkHello -16 15,991,854 71.6 NS/OP . 5 B/OP . 1 of allocs/OP duplicated code

The meaning of each column of the benchmark report is as follows:

type BenchmarkResult struct { N int //Number of iterations T time.Duration //Time spent in benchmarking Bytes int64 //Number of bytes processed in one iteration MemAllocs uint64 //Total number of allocated memory MemBytes uint64 //Total number of bytes allocated memory } Copy code

If the benchmark requires some time-consuming configuration before running, you can use

b.ResetTimer()
First reset the timer, for example:

func BenchmarkHello (b *testing.B) { ... //Time-consuming operation b.ResetTimer() for i := 0 ; i <bN; i++ { fmt.Sprintf( "hello" ) } } Copy code

use

RunParallel
Test concurrency performance

func BenchmarkParallel (b *testing.B) { templ := template.Must(template.New( "test" ).Parse( "Hello, {{.}}!" )) b.RunParallel( func (pb *testing.PB) { var buf bytes.Buffer for pb.Next() { //All goroutines together, the loop is executed bN times in total buf.Reset() templ.Execute(&buf, "World" ) } }) } Copy code
$ go test -benchmem -bench. ... BenchmarkParallel -16 3325430 375 ns/op 272 B/op 8 allocs/op ... Copy code