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
}
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)
}
}
}
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
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)
}
})
}
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
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)
...
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
}
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
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)
}
})
}
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
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!
Top comments (0)