Understand Go subtest in one article

Note: The first picture in this article is based on secondary processing of pictures generated by Lexica AI .

Permalink to this article – https://ift.tt/VYQz2qF

Unit testing is a crucial part of software development. Its significance includes but not limited to the following aspects:

  • Improve code quality: Unit testing can ensure the correctness, reliability and stability of the code, thereby reducing code defects and bugs.
  • Reduce the cost of regression testing: When modifying the code, the unit test can quickly check whether the functions of other modules are affected, avoiding the cost of regression testing of the entire system.
  • Promote teamwork: Unit testing can help team members better understand and use the code written by each other, improving the readability and maintainability of the code.
  • Improve development efficiency: unit testing can automate the execution of tests, reducing the time and workload of manual testing, thereby improving development efficiency.

The Go language designers decided at the beginning of Go design that language features and environment features should be grasped with both hands, and both hands must be hard. Facts have proved that the success of Go is precisely because of its focus on the overall environment of engineering software projects . The fact that Go has a built-in lightweight testing framework is also a reflection of Go’s emphasis on environmental features. Moreover, the Go team has continued to invest in this built-in testing framework, and more convenient and flexible new features have been added to the Go testing framework, which can help Gopher better organize test code and execute tests more efficiently.

The subtest introduced by Go in Go 1.7 is a typical representative. The addition of subtest enables Gopher to apply the built-in go test framework more flexibly.

In this article, I will talk about subtest with you based on the cognition, understanding and use of subtest that I have learned in daily development.

1. Review of Go unit testing

In the Go language, unit testing is regarded as a first-class citizen . Combined with Go’s built-in lightweight testing framework, Go developers can easily write unit testing cases.

Go unit tests are usually placed in the same package as the code being tested, and the source file where the unit test is located ends with _test.go, which is required by the Go test framework. The test function is prefixed with Test, accepts a parameter of type *testing.T, and uses methods such as t.Error, t.Fail, and t.Fatal to report test failures. All test code can be run with the go test command. If the test passes, output a message indicating that the test succeeded; otherwise, output an error message indicating which tests failed.

Note: Go also supports benchmarking, example testing, fuzzing, etc. for performance testing and documentation generation, but these are not the focus of this post.

Note: t.Error <=> t.Log+t.Fail

Usually when writing Go test code, we will first consider top-level test.

2. Go top-level test

The above-mentioned function starting with Test in *_test.go in the same directory as the source code under test is Go top-level test. In *_test.go, one or more functions starting with Test can be defined to test the functions or methods in the source code under test. For example:

 // https://github.com/bigwhite/experiments/blob/master/subtest/add_test.go // 被测代码,仅是demo func Add(a, b int) int { return a + b } // 测试代码func TestAdd(t *testing.T) { got := Add(2, 3) if got != 5 { t.Errorf("Add(2, 3) got %d, want 5", got) } } func TestAddZero(t *testing.T) { got := Add(2, 0) if got != 2 { t.Errorf("Add(2, 0) got %d, want 2", got) } } func TestAddOppositeNum(t *testing.T) { got := Add(2, -2) if got != 0 { t.Errorf("Add(2, -2) got %d, want 0", got) } }

Note: “got-want” is a common naming convention in Errorf in Go test

The execution of top-level test has the following characteristics:

  • go test will execute each TestXxx in a separate goroutine, keeping mutual isolation ;
  • If a TestXxx use case has not passed, the execution of other TestXxx will not be affected by Errorf or even Fatalf outputting wrong results;
  • A result of a TestXxx use case has not been judged, if an error result is output through Errorf, the TestXxx will continue to execute;
  • However, if TestXxx uses Fatal/Fatalf, this will cause the execution of TestXxx to end immediately at the position where Fatal/Fatalf is called, and the subsequent test code in the TestXxx function body will not be executed;
  • By default, each TestXxx is executed one by one in the order of declaration, even if they are executed in their respective goroutines;
  • Through go test -shuffle=on, each TestXxx can be executed in a random order, so that it can detect whether there is a dependency on the execution order between each TestXxx, and we need to avoid such dependencies in the test code;
  • Through the “go test -run=regular formula”, you can choose to execute some TestXxx.
  • Each TestXxx function can call the t.Parallel method (that is, the testing.T.Parallel method) to add TestXxx to the set of use cases that can be executed in parallel. Note: After being added to the set of parallel execution, the execution order of these TestXxx is uncertain.

Combined with the table-driven test that belongs to the best practice of Go (as shown in the following code TestAddWithTable), we can use the following TestAddWithTable to realize the equivalent test of the above three TestXxx without writing a lot of TestXxx:

 func TestAddWithTable(t *testing.T) { cases := []struct { name string a int b int r int }{ {"2+3", 2, 3, 5}, {"2+0", 2, 0, 2}, {"2+(-2)", 2, -2, 0}, //... ... } for _, caze := range cases { got := Add(caze.a, caze.b) if got != caze.r { t.Errorf("%s got %d, want %d", caze.name, got, caze.r) } } }

Go top-level test can meet most of Gopher’s conventional unit test requirements, and the table-driven conventions are also very easy to understand.

However, while the test based on top-level test+table drive simplifies the writing of test code, it also brings some disadvantages:

  • The cases in the table are executed sequentially and cannot be shuffle;
  • All cases in the table are executed in the same goroutine, and the isolation is poor;
  • If fatal/fatalf is used, once a case fails, subsequent test entries (cases) will not be executed;
  • The test case in the table cannot be executed in parallel;
  • The organization of test cases can only be tiled, not flexible enough to establish a hierarchy.

Go version 1.7 introduces subtest for this!

3. Subtest

Subtest in Go language refers to the function of dividing a test function (TestXxx) into multiple small test functions, each of which can run independently and report test results. This test method can control the test cases at a finer granularity, which is convenient for locating problems and debugging.

The following is a sample code that uses subtest to transform TestAddWithTable, showing how to write subtest in Go language:

 // https://github.com/bigwhite/experiments/blob/master/subtest/add_sub_test.go func TestAddWithSubtest(t *testing.T) { cases := []struct { name string a int b int r int }{ {"2+3", 2, 3, 5}, {"2+0", 2, 0, 2}, {"2+(-2)", 2, -2, 0}, //... ... } for _, caze := range cases { t.Run(caze.name, func(t *testing.T) { t.Log("g:", curGoroutineID()) got := Add(caze.a, caze.b) if got != caze.r { t.Errorf("got %d, want %d", got, caze.r) } }) } }

In the above code, we define a test function called TestAddWithSubtest, and use the t.Run() method in combination with the table test method to create three subtests, so that each subtest can reuse the same error handling logic , but the difference is reflected by the different test case parameters. Of course, if you don’t use table-driven tests, then each subtest can also have its own independent error handling logic!

Execute the test case TestAddWithSubtest above (we deliberately changed the implementation of the Add function to be wrong), and we will see the following results:

 $go test add_sub_test.go --- FAIL: TestAddWithSubtest (0.00s) --- FAIL: TestAddWithSubtest/2+3 (0.00s) add_sub_test.go:54: got 6, want 5 --- FAIL: TestAddWithSubtest/2+0 (0.00s) add_sub_test.go:54: got 3, want 2 --- FAIL: TestAddWithSubtest/2+(-2) (0.00s) add_sub_test.go:54: got 1, want 0

We can see that in the error message output, each failure case is identified by “TestXxx/subtestName”, and we can easily associate it with the corresponding code line. The deeper meaning is that subtest gives the entire test organization form a “sense of hierarchy”! Through the -run flag, we can select a certain/some Subtest of a top-level test to be executed in this “level”:

 $go test -v -run TestAddWithSubtest/-2 add_sub_test.go === RUN TestAddWithSubtest === RUN TestAddWithSubtest/2+(-2) add_sub_test.go:51: g: 19 add_sub_test.go:54: got 1, want 0 --- FAIL: TestAddWithSubtest (0.00s) --- FAIL: TestAddWithSubtest/2+(-2) (0.00s) FAIL FAIL command-line-arguments 0.006s FAIL

Let’s take a look at the characteristics of subtest (you can compare it with the previous top-level test):

  • go subtest will also be executed in a separate goroutine, keeping mutual isolation ;
  • If a Subtest case has not passed, the error results output through Errorf or even Fatalf will not affect the execution of other Subtests under the same TestXxx;
  • A certain result in a Subtest has not been judged, if an error result is output through Errorf, the Subtest will continue to execute;
  • However, if the subtest uses Fatal/Fatalf, this will cause the execution of the subtest to end immediately at the place where Fatal/Fatalf is called, and the subsequent test code in the subtest function body will not be executed;
  • By default, the subtests under each TestXxx will be executed one by one in the order of declaration, even if they are executed in their respective goroutines;
  • So far, subtest does not support random order execution in shuffle mode ;
  • By “go test -run=TestXxx/regular expression[/…]”, we can choose to execute one or some subtests under TestXxx;
  • Each subtest can call the t.Parallel method (that is, the testing.T.Parallel method) to add the subtest to the set of use cases that can be executed in parallel. Note: After being added to the set of parallel execution, the execution order of these subtests is uncertain.

In summary, the advantages of subtest can be summarized as follows:

  • Finer-grained testing: By breaking a test case into multiple small test functions, it is easier to locate and debug problems.
  • Better readability: subtest can make test code clearer and easier to understand.
  • More flexible testing: subtests can be combined and arranged as needed to meet different testing needs.
  • More hierarchical organization of test code: Through subtest, a more hierarchical test code organization can be designed, sharing resources more conveniently and setting setup and teardown at a certain organizational level, my “Go Language Improvement Road” vol2 Article 41 “Organize test code hierarchically” has a systematic description of this aspect, and you can refer to it.

4. Subtest vs. top-level test

The top-level test itself is actually a subtest, but its scheduling and execution are controlled by the Go test framework, which is not visible to our developers.

For gophers:

  • Simple package testing can be satisfied in the top-level test, which is direct, intuitive and easy to understand.
  • For complex packages in slightly larger projects, once it comes to the hierarchical design of test code organization, the organization, flexibility and scalability of subtest can better help us improve test efficiency and reduce test time .

Note: A small part of this article comes from the answers generated by ChatGPT .

The source code involved in this article can be downloaded here .


“Gopher Tribe” knowledge planet aims to create a boutique Go learning and advanced community! High-quality first release of Go technical articles, “three days” first reading right, analysis of the development status of Go language twice a year, fresh Gopher daily 1 hour in advance every day, online courses, technical columns, book content preview, must answer within six hours Guaranteed to meet all your needs about the Go language ecology! In 2023, the Gopher tribe will further focus on how to write elegant, authentic, readable, and testable Go code, pay attention to code quality and deeply understand Go core technology, and continue to strengthen interaction with star friends. Everyone is welcome to join!

img{512x368}

img{512x368}

img{512x368}

img{512x368}

The well-known cloud hosting service provider DigitalOcean released the latest hosting plan. The entry-level Droplet configuration is upgraded to: 1 core CPU, 1G memory, 25G high-speed SSD, and the price is 5$/month. Friends who need to use DigitalOcean, you can open this link address : https://ift.tt/ePa3Njb to start your DO host road.

Gopher Daily archive repository – https://ift.tt/Xj1wSbp

my contact information:

  • Weibo (temporarily unavailable): https://ift.tt/NDkHmLs
  • Weibo 2: https://ift.tt/3SCWYXK
  • Blog: tonybai.com
  • github: https://ift.tt/XM4Up5j

Business cooperation methods: writing, publishing, training, online courses, partnerships, consulting, advertising cooperation.

© 2023, bigwhite . All rights reserved.

This article is transferred from https://tonybai.com/2023/03/15/an-intro-of-go-subtest/
This site is only for collection, and the copyright belongs to the original author.