Go has a built-in testing framework that allows you to write complete tests without introducing external packages. This article takes a look at the native testing capabilities that Go offers, its limitations, and ways to complement them.

1. Basic testing framework

In the Go language, all tests need to end with _test. Go, so that Go build does not compile files ending in _test. Go, whereas Go test compiles files ending in _test. Go.

We all use the Testing package when we write tests. In this package, common types include the following:

  • testing.T
  • testing.B
  • testing.M

Testing.TB and testing.PB are not commonly used, so we will not expand them here, and those who are interested can search by themselves. The three types above represent three different tests, unit tests, benchmark tests, and TestMain tests, which must be included in test method inputs for different tests.

An exception to this rule is the Example test, which is used to print test cases in a document. The Example test must start with Example, the method takes no arguments, and specifies the output of the instance, as follows:

func ExampleTest(a) {
	fmt.Println("run example test")
	// Output:
	// run example test
}
Copy the code

All tests can be initiated using go test. For example, a test can be initiated under the current package. Go test -v./, -v indicates the process of printing tests.

Unit testing

Unit tests are written according to certain rules. All unit tests should start with Test followed by the name of the Test method, as follows:

import (
	"fmt"
	"testing"
)

func TestDemo(t *testing.T) {
	fmt.Println(" run test demo")}Copy the code

This is the simplest unit test. In practice, a set of tests may have multiple unit tests running at the same time. In this case, we need a method to string these tests together.

func TestMain(m *testing.M)  {
	fmt.Println("begin test")
	m.Run()
	fmt.Println("end test")}Copy the code

TestMain(m * testing.m) can have only one TestMain(m * testing.m) in a package. During Test execution, this method is executed first, and then all Test and Example tests in the package are executed.

In addition, this method can be used to initialize and reclaim resources. Some tests need to initialize some configuration before running, connect to the database, release the database connection, etc., can be completed in this test.

After writing the above tests, you can run the Test, which will run all the Test and Example tests, starting from TestMain:

$ go test -v ./
Copy the code

But sometimes we also care about unit test coverage and can see it by adding a parameter:

$ go test -cover -v ./
Copy the code

3. Benchmark

Benchmarks are usually used to test the performance of an application. They must start with a Benchmark and use testing.b as an input, as follows:

func BenchmarkDemo(b *testing.B) {
	fmt.Println("run benchmark demo")}Copy the code

To better illustrate the functionality of a benchmark, I use an example from a previous benchmark that tested string concatenation:

func BenchmarkPlus(b *testing.B) {
	str := "this is just a string"

	for i := 0; i < b.N; i++ {
		stringPlus(str)
	}
}

func stringPlus(str string) string {
	s := ""
	for i := 0; i < 10000; i++ {
		s += str
	}
	return s
}
Copy the code

Where b.N is not a fixed value, the size of this value is determined by the framework itself. The content of the above test is to test the performance of a function that needs to concatenate ten thousand character passes, and the number of times the test is run is determined by the framework itself.

To run the benchmark, run the following command:

$ go test -bench=. -benchmem .
Copy the code

The following output is displayed:

goos: darwin goarch: amd64 pkg: zxin.com/zx-demo/string_benchmark **BenchmarkPlus-12 12 96586447 ns/op 1086401355 B/op 10057 allocs/op** PASS ok Zxin.com/zx-demo/string_benchmark 6.186 sCopy the code

The line in bold is the output from the benchmark, and the information in each column is as follows:

  • The first column represents the method name of the benchmark and the value of the GOMAXPROCS used
  • The second column represents the number of test cycles
  • The third column shows the average time taken per test in nanoseconds
  • The fourth column represents the average amount of memory allocated per run
  • The fifth column represents the number of times memory is allocated for each run

4. Test enhancement

But the native test package is not perfect, for example, in the unit test, there is a lack of assertion mechanism, which makes it very inconvenient to judge the test results, there is an external package can help improve the test function.

Installation is also very convenient:

$ go get github.com/stretchr/testify
Copy the code

This package extends the capabilities of the Go native testing framework in three ways:

  • assertions

All of the assertions are missing from the native testing framework, which is inconvenient in many situations. All of the assertions are good to use out of the box with the native testing framework:

func TestAssert(t *testing.T) {
	assert := assert.New(t)

	assert.Equal(123.123."they should be equal")

	assert.NotEqual(123.456."they should not be equal")

	o := make(map[string]string)
	o["ray"] = "jun"

	if assert.NotNil(o) {
		assert.Equal("jun", o["ray"])}else {
		assert.Nil(o)
	}
}
Copy the code
  • The Mock ability

Potency provides the ability to Mock the data a test needs, which is useful for tests that require complex data:

type MyMockedObject struct{
	mock.Mock
}

func (m *MyMockedObject) DoSomething(number int) (bool, error) {
	args := m.Called(number)
	return args.Bool(0), args.Error(1)}func TestSomething(t *testing.T) {
	testObj := new(MyMockedObject)

	testObj.On("DoSomething".123).Return(true.nil)

	testMockObj(testObj)

	testObj.AssertExpectations(t)
}

func testMockObj(mcObj *MyMockedObject) {
	fmt.Println(mcObj.DoSomething(123))}Copy the code
  • Build better tests

Even if TestMain is used to initialize the configuration, it is not flexible enough. For example, I need to have multiple sets of tests in a package, each of which is initialized differently, but the Suite package provides a more object-oriented approach. It also provides setup/teardown methods to initialize and reclaim resources, which can be directly tested using Go Test without intrusive changes to the existing testing framework:

type ExampleTestSuite struct {
	suite.Suite
	VariableThatShouldStartAtFive int
}

func (suite *ExampleTestSuite) SetupTest(a) {
	fmt.Println("run setup method")
	suite.VariableThatShouldStartAtFive = 5
}

func (suite *ExampleTestSuite) TearDownTest(a) {
	fmt.Println("run tear down method")
	suite.VariableThatShouldStartAtFive = 0

}

func (suite *ExampleTestSuite) TestExample(a) {
	assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive)
}

func TestExampleTestSuite(t *testing.T) {
	suite.Run(t, new(ExampleTestSuite))
}
Copy the code

5. Summary

Although the Go native testing framework already supports writing complex tests, there are many scenarios where it is not convenient to use a new test enhancement package, all of which is useful out of the box and does not break the existing testing process.

The text/Rayjun