Go - Creating Subtests to Have Finer Control Over Groups of Test Cases

发布时间 2023-10-18 16:27:45作者: ZhangZhihuiAAA

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