Problem: You want to create subtests within a test function to have finer control over test cases.
Solution: Use the t.Run function to create subtests within a test function. Subtests extend the flexibility of test functions to another level down.
When using table-driven tests, you often want to run specific tests or have finer-grained control over the test cases. However, table-driven tests are really driven through data so there is no way you can control it. For example, in the previous test function using table-driven tests you can only test the results for nonequality for every single test case:
func TestAddWithTables(t *testing.T) { testCases := []struct { a int b int result int }{ {1, 2, 3}, {12, 30, 42}, {100, -1, 99}, } for _, testCase := range testCases { result := Add(testCase.a, testCase.b) if result != testCase.result { t.Errorf("Adding %d and %d doesn't produce %d, instead it produces %d", testCase.a, testCase.b, testCase.result, result) } } }
Go 1.7 added a new feature to allow subtests within a test function, using the t.Run function. This is how you can turn your table-driven test function into one that uses subtests:
func TestAddWithSubTest(t *testing.T) { testCases := []struct { name string a int b int result int }{ {"OneDigit", 1, 2, 3}, {"TwoDigits", 12, 30, 42}, {"ThreeDigits", 100, -1, 99}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { result := Add(testCase.a, testCase.b) if result != testCase.result { t.Errorf("Adding %d and %d doesn't produce %d, instead it produces %d", testCase.a, testCase.b, testCase.result, result) } }) } }
As you can see, you added a test name to each of the test cases. Then within the for loop, you call t.Run , passing it the test case name and also calling an anonymous function that has the same form as a normal test function. This is the subtest from which you can run each test case.
Run the test now and see what happens:
% go test -v -run TestAddWithSubTest
=== RUN TestAddWithSubTest
=== RUN TestAddWithSubTest/OneDigit
=== RUN TestAddWithSubTest/TwoDigits
=== RUN TestAddWithSubTest/ThreeDigits
- - - PASS: TestAddWithSubTest (0.00s)
- - - PASS: TestAddWithSubTest/OneDigit (0.00s)
- - - PASS: TestAddWithSubTest/TwoDigits (0.00s)
- - - PASS: TestAddWithSubTest/ThreeDigits (0.00s)
PASS
ok github.com/sausheong/gocookbook/ch18_testing 0.607s
As you can see, each subtest is named and run separately under the umbrella test function. You could, in fact, pick and choose the subtest you want to run:
% go test -v -run TestAddWithSubTest/TwoDigits
=== RUN TestAddWithSubTest
=== RUN TestAddWithSubTest/TwoDigits
- - - PASS: TestAddWithSubTest (0.00s)
- - - PASS: TestAddWithSubTest/TwoDigits (0.00s)
PASS
ok github.com/sausheong/gocookbook/ch18_testing 0.193s
In the preceding example, you didn’t do anything more than what you did before, but you could have done more to customize the setup and teardown. For example:
func TestAddWithCustomSubTest(t *testing.T) { testCases := []struct { name string a int b int result int setup func() teardown func() }{ {"OneDigit", 1, 2, 3, func() { fmt.Println("setup one") }, func() { fmt.Println("teardown one") }}, {"TwoDigits", 12, 30, 42, func() { fmt.Println("setup two") }, func() { fmt.Println("teardown two") }}, {"ThreeDigits", 100, -1, 99, func() { fmt.Println("setup three") }, func() { fmt.Println("teardown three") }}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { testCase.setup() defer testCase.teardown() result := Add(testCase.a, testCase.b) if result != testCase.result { t.Errorf("Adding %d and %d doesn't produce %d, instead it produces %d", testCase.a, testCase.b, testCase.result, result) } else { fmt.Println(testCase.name, "ok.") } }) } }
If you run it now, you can see that each subtest has its own setup and teardown functions that are called separately:
% go test -v -run TestAddWithCustomSubTest
=== RUN TestAddWithCustomSubTest
=== RUN TestAddWithCustomSubTest/OneDigit
setup one
OneDigit ok.
teardown one
=== RUN TestAddWithCustomSubTest/TwoDigits
setup two
TwoDigits ok.
teardown two
=== RUN TestAddWithCustomSubTest/ThreeDigits
setup three
ThreeDigits ok.
teardown three
- - - PASS: TestAddWithCustomSubTest (0.00s)
- - - PASS: TestAddWithCustomSubTest/OneDigit (0.00s)
- - - PASS: TestAddWithCustomSubTest/TwoDigits (0.00s)
- - - PASS: TestAddWithCustomSubTest/ThreeDigits (0.00s)
PASS
ok github.com/sausheong/gocookbook/ch18_testing 0.278s
In the previous examples, you used subtests on a table-driven test. However, it doesn’t need to be table-driven. Subtests can be used simply to group different tests under a single test function. For example, in the following case, you want to group the tests on the flip function under a single test function:
func TestFlipWithSubTest(t *testing.T) { grid := load("monalisa.png") // setup for all flip tests t.Run("CheckPixels", func(t *testing.T) { p1 := grid[0][0] flip(grid) defer flip(grid) // teardown for check pixel to unflip the grid p2 := grid[0][479] if p1 != p2 { t.Fatal("Pixels not flipped") } }) t.Run("CheckDimensions", func(t *testing.T) { flip(grid) save("flipped.png", grid) // teardown for check dimensions to remove the file defer os.Remove("flipped.png") g := load("flipped.png") if len(g) != 321 || len(g[0]) != 480 { t.Error("Grid is wrong size", "width:", len(g), "length:", len(g[0])) } }) }
In this case, you have a single setup for all the flip tests but different teardowns for individual test cases. Each test case can be run as a different test function, but grouping them together allows you to have a one-time setup for test fixtures and also to run multiple subtests under a single umbrella:
% go test -v -run TestFlipWithSubTest
=== RUN TestFlipWithSubTest
=== RUN TestFlipWithSubTest/CheckPixels
=== RUN TestFlipWithSubTest/CheckDimensions
- - - PASS: TestFlipWithSubTest (0.07s)
- - - PASS: TestFlipWithSubTest/CheckPixels (0.00s)
- - - PASS: TestFlipWithSubTest/CheckDimensions (0.05s)
PASS
ok github.com/sausheong/gocookbook/ch18_testing 0.269s