Testing with WaitGroups
Writing tests for concurrent code can be challenging because of the non-deterministic nature of concurrent execution. WaitGroups are essential for reliably testing goroutines and ensuring all concurrent operations complete before making assertions.
Why Testing Concurrent Code is Challenging#
Concurrent code introduces several testing challenges:
- Non-determinism: Goroutines may execute in different orders each time
- Race conditions: Shared resources might be accessed simultaneously
- Timing issues: Tests may pass or fail based on execution timing
- Deadlocks: Tests might hang if goroutines don't complete
WaitGroups help address these challenges by providing a way to ensure all goroutines complete before making assertions.
Basic Pattern for Testing with WaitGroups#
Here's a general pattern for testing concurrent code with WaitGroups:
- Create a WaitGroup
- Set up the test case and expected results
- Launch goroutines and add them to the WaitGroup
- Wait for all goroutines to complete
- Make assertions on the results
Testing a Simple Concurrent Function#
Let's test a simple function that prints a message using a goroutine:
package main
import (
string">"io"
string">"os"
string">"strings"
string">"sync"
string">"testing"
)
func printSomething(s string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println(s)
}
func Test_printSomething(t *testing.T) {
string">"comment">// Capture stdout
stdOut := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
string">"comment">// Create a WaitGroup and run the function
var wg sync.WaitGroup
wg.Add(1)
go printSomething(string">"ok print", &wg)
wg.Wait()
string">"comment">// Restore stdout and read the output
w.Close()
result, _ := io.ReadAll(r)
output := string(result)
os.Stdout = stdOut
string">"comment">// Make assertions
if !strings.Contains(output, string">"ok print") {
t.Errorf(string">"Expected output to contain string">'ok print' but got %s", output)
}
}
In this test:
- We capture standard output to be able to test what was printed
- We create a WaitGroup and run our function in a goroutine
- We wait for the goroutine to complete
- We restore standard output and check the captured output
Testing Message Updates#
Let's test a concurrent function that updates a shared message:
package main
import (
string">"sync"
string">"testing"
)
var msg string
func updateMessage(s string, wg *sync.WaitGroup) {
defer wg.Done()
msg = s
}
func Test_updateMessage(t *testing.T) {
string">"comment">// Set initial message
msg = string">"Hello, world!"
string">"comment">// Create a WaitGroup and run the function
var wg sync.WaitGroup
wg.Add(1)
go updateMessage(string">"Hello, one", &wg)
wg.Wait()
string">"comment">// Check the result
if msg != string">"Hello, one" {
t.Errorf(string">"Expected message to be string">'Hello, one' but got string">'%s'", msg)
}
}
Testing with Multiple Goroutines#
When testing functions that launch multiple goroutines, make sure to set the WaitGroup counter correctly:
func processItems(items []string) []string {
var wg sync.WaitGroup
results := make([]string, len(items))
for i, item := range items {
wg.Add(1)
go func(i int, item string) {
defer wg.Done()
results[i] = string">"Processed: " + item
}(i, item)
}
wg.Wait()
return results
}
func Test_processItems(t *testing.T) {
items := []string{string">"one", string">"two", string">"three"}
results := processItems(items)
string">"comment">// Make assertions
expected := []string{
string">"Processed: one",
string">"Processed: two",
string">"Processed: three",
}
for i, exp := range expected {
if results[i] != exp {
t.Errorf(string">"Expected %s but got %s at position %d", exp, results[i], i)
}
}
}
Testing for Race Conditions#
Go provides a race detector that can help identify race conditions in your tests. Run your tests with the -race
flag:
go test -race ./...
Here's an example of a test that would catch a race condition:
func Test_concurrentAccess(t *testing.T) {
counter := 0
var wg sync.WaitGroup
string">"comment">// Launch 100 goroutines that all increment counter
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ string">"comment">// Race condition: multiple goroutines access counter simultaneously
}()
}
wg.Wait()
if counter != 100 {
t.Errorf(string">"Expected counter to be 100, but got %d", counter)
}
}
When run with -race
, this test will detect the race condition on counter
.
Testing Timeout to Prevent Hangs#
To prevent tests from hanging, you can use the testing package's built-in timeout mechanisms:
func Test_possiblyHangingFunction(t *testing.T) {
string">"comment">// Set a timeout of 5 seconds for this test
t.Parallel()
done := make(chan bool)
go func() {
string">"comment">// Run the test
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
string">"comment">// Potentially long operation
}()
wg.Wait()
done <- true
}()
select {
case <-done:
string">"comment">// Test completed in time
case <-time.After(5 * time.Second):
t.Fatal(string">"Test timed out after 5 seconds")
}
}
Best Practices for Testing with WaitGroups#
- Always Wait: Always call
wg.Wait()
before making assertions - Test for Race Conditions: Use the
-race
flag to detect race conditions - Set Timeouts: Prevent tests from hanging with appropriate timeouts
- Capture Output: Redirect output when testing functions that print
- Use Deterministic Input: When possible, use input that produces deterministic results
- Test Edge Cases: Test with zero goroutines, many goroutines, etc.
- Run Tests Multiple Times: Run tests multiple times to catch timing-dependent issues
Summary#
- WaitGroups are essential for testing concurrent code
- They ensure that all goroutines complete before making assertions
- Go's race detector can help identify race conditions
- Proper test setup is crucial for reliable concurrent tests
- Testing concurrent code requires handling non-determinism and timing issues
Testing concurrent code is challenging, but with WaitGroups and the right patterns, you can write reliable tests that catch issues in your concurrent code.
In the next lab, we'll explore race conditions in more detail and learn how to detect and fix them.