In this article, we will introduce you to Golang unit testing. We will take a look at how to write tests in the Golang world by referring to different materials and manual demos, and warm up for Golang TDD.

Commonly used libraries

testing // The system has its own testing library (required)
Copy the code
go get github.com/stretchr/testify / / assert that library
Copy the code

The Potency library lets you write a more readable test assertion:

A simple example

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

example/
|--calc.go  
|--calc_test.go 
Copy the 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 the code

calc_test.go

package main

import "testing"

/** CD to the unittest directory, then run the following command: go test To display the details of whether each test method was successfully verified: go test -v To specify that only one test should be run: Go test-run TestAdd -v t.fatal/t.falf if there is an error stop t.elror/t.elrorf if there is an error stop */
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 the code

How do I run tests?

CD to the unittest directory and run the following command: go test To display whether each test method was successfully verified in detail: go test -v To specify that only one test should be run: go test-run TestAdd -v

In addition:

  • T.F atal/t.F alf stop at any mistake

  • T.ror/t.rorf will not stop when errors occur

See here for detailed commands:

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 // specifies a single unit use case for the file to run. '-v' prints details go test -v -bench=.benchmark_test. go // Specifies a performance case for the file to run. Bench =. -benchtime=5s benchmark_test.go // '-benchtime=5s' Default 1s go test -v bench=Alloc benchmem benchmark_test. -v cover // Run all test files in the current directory and subdirectories and display details and coverage rateCopy the code

Subtest child test

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

Key statements:

t.Run
Copy the code

Table – driven tests test

We prefer this:

demo2.go

package main

func MergeString(x, y string) string {
  return x + y
}
Copy the 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) {
      ifans:= MergeString(test.X, test.Y); ans! =test.Expected { t.Error("fail")}})}}Copy the code

The data for all use cases is organized in slicing cases, which look like a table, with loops to create child tests. The advantages of writing this way are:

  • Adding a use case is as simple as adding a piece of test data to Cases.

  • The test code is readable, visually showing the parameters and expected return values of each child test.

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

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

Helper function Helper

demo3_test.go

package main

import "testing"

type myCase struct {
  Str string
  Expected string
}

// Help function: used to refactor some common code
func createFirstLetterToUpperCase(t *testing.T, c *myCase) {
  // To.helper () is used to print the line number of the error when running the go test
  t.Helper()
  ifans := 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 the code

demo3.go

package main

import "strings"

func FirstLetterToUpperCase(x string) string {
  return strings.ToUpper(x[:1]) + x[1:]}Copy the code

Key syntax:

to.Helper() // Print the line number of the error when running the go test
Copy the code

Two suggestions for helper functions:

  • Don’t return an error, just use t. ror or t. atal inside the help function. There won’t be too much error-handling code in the main logic of the use case that will affect the readability.

  • Calling t.helper () makes the error message more accurate and helps locate.

Setup and teardown

These are the life cycle functions of test

Setup does some initialization, teardown does some reclamation.

func setup(a) {
  fmt.Println("Before all tests")}func teardown(a) {
  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 the code

Description:

  • In this test file, there are two test cases, Test1 and Test2.

  • If the test file contains the function TestMain, the generated test will call TestMain(m) instead of running the test directly.

  • The call to m.run () triggers the execution of all test cases, and os.exit () processes the returned status code. If it is not 0, the useful case fails.

  • So you can do some extra setup and teardown before and after the call to m.run ().

Executing go test will print:

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

Copy the code

Network Testing

TCP/HTTP

Assume that a handler that needs to test an API interface, such as helloHandler, works

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

Then we can create a real network connection to test:

// test code
import (
  "io/ioutil"
  "net"
  "net/http"
  "testing"
)

func handleError(t *testing.T, err error) {
  t.Helper()
  iferr ! =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 the code
  • Net.listen (” TCP “, “127.0.0.1:0”) : Listens on an unused port and returns a Listener.

  • Call http.serve (ln, nil) to start the HTTP service.

  • Make a Get request using http.Get to check that the value returned is correct.

  • Try not to mock HTTP and NET libraries so that you can cover more realistic scenarios.

httptest

For HTTP development scenarios, it is more efficient to use 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 the code

Httptest is used to simulate the request object (REQ) and response object (W) to achieve the same goal.

Benchmark Test

Benchmark test cases are defined as follows:

func BenchmarkName(b *testing.B){
    // ...  
}
Copy the code
  • The function name must start with Benchmark, followed by the name of the function to be tested

  • The parameter is b * testing.b.

  • When benchmarking, you need to add the -bench parameter.

Such as:

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

Run: go test-benchmem-bench.

BenchmarkHello- 16   15991854   71.6 ns/op   5 B/op   1 allocs/op    
Copy the code

The values for each column in the benchmark report have the following meanings:

type BenchmarkResult struct {  
    N         int           // Number of iterations
    T         time.Duration // The time taken by the benchmark
    Bytes     int64         // The number of bytes processed in an iteration
    MemAllocs uint64        // Total number of memory allocations
    MemBytes  uint64        // Total number of bytes allocated memory
}
Copy the code

If the benchmark requires some time-consuming configuration before running, you can use b.resetTimer () to reset the timer, for example:

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

Use RunParallel to 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 a total of b.N times
    buf.Reset()
    templ.Execute(&buf, "World")}}}Copy the code
$ go test -benchmem -bench .
...
BenchmarkParallel- 16   3325430     375 ns/op   272 B/op   8 allocs/op
...
Copy the code