DEV Community

Clavin June
Clavin June

Posted on • Originally published at clavinjune.dev on

Speed Up Golang Test Execution Time

Photo by @chrisliverani on Unsplash

Introduction

Code Testing is a requirement for production codes. Some people ignore it, some people write it. Why do some people don't write tests? One of the reasons is it takes time to think and write the test cases, and it may slow down the development and CI/CD process. When you think of good test cases and corner cases, you may produce tons of test cases to handle. Some tests complete in no time, some are not.

To help developers handle this issue, Golang provides a parallelization inside its standard library. You can do a parallel test without any 3rd party libraries or create a goroutine manually. In this article, you will learn how to make a parallel test to speed up the execution of the test.

Function to be Tested

Let's say you have this time-based EvenOrSleep function which you may use on a login failed event to avoid spam or brute-force.

var ErrNotEven = errors.New("not an even number")

func EvenOrSleep(n int) error {
    if n%2 == 0 {
        time.Sleep(time.Duration(n) * time.Second)
        return nil
    }

    return ErrNotEven
}
Enter fullscreen mode Exit fullscreen mode

Unit Test Function

func TestEvenOrSleep(t *testing.T) {
    tt := []struct {
        n        int
        expected error
    }{
        {1, ErrNotEven},
        {3, ErrNotEven},
        {5, ErrNotEven},
        {2, nil},
        {4, nil},
    }

    for _, tc := range tt {
        actual := EvenOrSleep(tc.n)

        if tc.expected != actual {
            t.Errorf(`expected "%v", actual "%v"`, tc.expected, actual)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you run the test, it will take around 6 seconds to execute all the test cases.

 $ go test -v
=== RUN   TestEvenOrSleep
-------- PASS: TestEvenOrSleep (6.00s)
PASS
ok      example 6.004s
Enter fullscreen mode Exit fullscreen mode

But, if you make the test cases parallel, It will take around 4 seconds because all the test cases are running at the same time. So let's improve the test using t.Parallel().

Modified Unit Test Function

for _, tc := range tt {
    t.Run(fmt.Sprint(tc.n), func(t *testing.T) {
        t.Parallel()
        actual := EvenOrSleep(tc.n)

        if tc.expected != actual {
            t.Errorf(`expected "%v", actual "%v"`, tc.expected, actual)
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

You only need to change the code inside the iteration. Make all the test cases become subtests using t.Run and name them using their input tc.n. And then, inside the subtest, call the t.Parallel() function so it makes Golang run all the subtests at the same time.

$ go test -v
=== RUN   TestEvenOrSleep
=== RUN   TestEvenOrSleep/1
=== PAUSE TestEvenOrSleep/1
=== RUN   TestEvenOrSleep/3
=== PAUSE TestEvenOrSleep/3
=== RUN   TestEvenOrSleep/5
=== PAUSE TestEvenOrSleep/5
=== RUN   TestEvenOrSleep/2
=== PAUSE TestEvenOrSleep/2
=== RUN   TestEvenOrSleep/4
=== PAUSE TestEvenOrSleep/4
=== CONT  TestEvenOrSleep/1
=== CONT  TestEvenOrSleep/5
=== CONT  TestEvenOrSleep/3
=== CONT  TestEvenOrSleep/2
=== CONT  TestEvenOrSleep/4
-------- PASS: TestEvenOrSleep (0.00s)
    --- PASS: TestEvenOrSleep/2 (4.00s)
    --- PASS: TestEvenOrSleep/4 (4.00s)
    --- PASS: TestEvenOrSleep/1 (4.00s)
    --- PASS: TestEvenOrSleep/5 (4.00s)
    --- PASS: TestEvenOrSleep/3 (4.00s)
PASS
ok      example 4.004s
Enter fullscreen mode Exit fullscreen mode

As you see, it takes around 4 seconds to complete the whole test. But there's something wrong. If you see the test output on lines 19-23, you will see that each test was executed for 4 seconds.

...
    --- PASS: TestEvenOrSleep/2 (4.00s)
    --- PASS: TestEvenOrSleep/4 (4.00s)
    --- PASS: TestEvenOrSleep/1 (4.00s)
    --- PASS: TestEvenOrSleep/5 (4.00s)
    --- PASS: TestEvenOrSleep/3 (4.00s)
...
Enter fullscreen mode Exit fullscreen mode

Let's add some log inside the EvenOrSleep function to debug it.

func EvenOrSleep(n int) error {
    log.Println("input", n)
    if n%2 == 0 {
        time.Sleep(time.Duration(n) * time.Second)
        return nil
    }

    return ErrNotEven
}
Enter fullscreen mode Exit fullscreen mode

And then run the test again without the -v flags to make the output less verbose.

$ go test
2021/11/15 17:22:13 input 4
2021/11/15 17:22:13 input 4
2021/11/15 17:22:13 input 4
2021/11/15 17:22:13 input 4
2021/11/15 17:22:13 input 4
PASS
ok      example 4.006s
Enter fullscreen mode Exit fullscreen mode

Why is it always 4? Because the tc is always mutated on each iteration. You can see my other blog post to fix this issue. Or you can use this alternative solution below.

for _, tc := range tt {
    tc := tc
    t.Run(fmt.Sprint(tc.n), func(t *testing.T) {
        t.Parallel()
        actual := EvenOrSleep(tc.n)

        if tc.expected != actual {
            t.Errorf(`expected "%v", actual "%v"`, tc.expected, actual)
        }
    })
}

// or

for i := range tt {
    tc := tt[i]
    t.Run(fmt.Sprint(tc.n), func(t *testing.T) {
        t.Parallel()
        actual := EvenOrSleep(tc.n)

        if tc.expected != actual {
            t.Errorf(`expected "%v", actual "%v"`, tc.expected, actual)
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

tc := tc copies the value of tc and makes it a new local variable so it won't be mutated on each iteration. Of course, you can change the tc to the other variable let's say newTc := tc, it doesn't need to be the same, just like creating a variable. Now let's remove the log, and re-run the tests again.

 $ go test -v
=== RUN   TestEvenOrSleep
=== RUN   TestEvenOrSleep/1
=== PAUSE TestEvenOrSleep/1
=== RUN   TestEvenOrSleep/3
=== PAUSE TestEvenOrSleep/3
=== RUN   TestEvenOrSleep/5
=== PAUSE TestEvenOrSleep/5
=== RUN   TestEvenOrSleep/2
=== PAUSE TestEvenOrSleep/2
=== RUN   TestEvenOrSleep/4
=== PAUSE TestEvenOrSleep/4
=== CONT  TestEvenOrSleep/1
=== CONT  TestEvenOrSleep/2
=== CONT  TestEvenOrSleep/3
=== CONT  TestEvenOrSleep/4
=== CONT  TestEvenOrSleep/5
-------- PASS: TestEvenOrSleep (0.00s)
    --- PASS: TestEvenOrSleep/1 (0.00s)
    --- PASS: TestEvenOrSleep/3 (0.00s)
    --- PASS: TestEvenOrSleep/5 (0.00s)
    --- PASS: TestEvenOrSleep/2 (2.00s)
    --- PASS: TestEvenOrSleep/4 (4.00s)
PASS
ok      example 4.003s
Enter fullscreen mode Exit fullscreen mode

It's fixed!

Conclusion

By using a parallel test, you just reduced your test execution time as well as your CI/CD process. The processes are easy, just modify your test into a subtest for each case, and call the t.Parallel() function. That's it!

Thank you for reading!

Discussion (0)