# Go: Testing Concurrent Applications


Testing concurrent programs is a lot like testing single-task programs. If the code is well-designed, you can test the state of a concurrent program with standard tools like channels, wait groups, and other abstractions built on top of them.

But if you've made it so far, you know that concurrency is never _that_ easy. In this chapter, we'll go over common testing problems and the solutions that Go offers.

---

## Waiting for Goroutines to Finish

Let's say we want to test this function:

```go
// Calc calculates something asynchronously.
func Calc() <-chan int {
    out := make(chan int, 1)
    go func() {
        out <- 42
    }()
    return out
}
```

Calculations run asynchronously in a separate goroutine. However, the function returns a result channel, so this isn't a problem:

```go
func Test(t *testing.T) {
    got := <-Calc() // (X)
    if got != 42 {
        t.Errorf("got: %v; want: 42", got)
    }
}
```

```
PASS
```

At point ⓧ, the test is guaranteed to wait for the inner goroutine to finish. The rest of the test code doesn't need to know anything about how concurrency works inside the `Calc` function. Overall, the test isn't any more complicated than if `Calc` were synchronous.

But we're lucky that `Calc` returns a channel. What if it doesn't?

### Naive Approach

Let's say the `Calc` function looks like this:

```go
var state atomic.Int32

// Calc calculates something asynchronously.
func Calc() {
    go func() {
        state.Store(42)
    }()
}
```

We write a simple test and run it:

```go
func TestNaive(t *testing.T) {
    Calc()
    got := state.Load() // (X)
    if got != 42 {
        t.Errorf("got: %v; want: 42", got)
    }
}
```

```
=== RUN   TestNaive
    main_test.go:27: got: 0; want: 42
--- FAIL: TestNaive (0.00s)
```

The assertion fails because at point ⓧ, we didn't wait for the inner `Calc` goroutine to finish. In other words, we didn't _synchronize_ the `TestNaive` and `Calc` goroutines. That's why `state` still has its initial value (0) when we do the check.

### Waiting with time.Sleep

We can add a short delay with `time.Sleep`:

```go
func TestSleep(t *testing.T) {
    Calc()

    // Wait for the goroutine to finish (if we're lucky).
    time.Sleep(50 * time.Millisecond)

    got := state.Load()
    if got != 42 {
        t.Errorf("got: %v; want: 42", got)
    }
}
```

```
=== RUN   TestSleep
--- PASS: TestSleep (0.05s)
```

The test is now passing. But using `time.Sleep` to sync goroutines isn't a great idea, even in tests. We don't want to set a custom delay for every function we're testing. Also, the function's execution time may be different on the local machine compared to a CI server. If we use a longer delay just to be safe, the tests will end up taking too long to run.

Sometimes you can't avoid using `time.Sleep` in tests, but since Go 1.25, the `synctest` package has made these cases much less common. Let's see how it works.

### Waiting with synctest

The `synctest` package has a lot going on under the hood, but its public API is very simple:

```go
func Test(t *testing.T, f func(*testing.T))
func Wait()
```

The `synctest.Test` function creates an isolated _bubble_ where you can control time to some extent. Any new goroutines started inside this bubble become part of the bubble. So, if we wrap the test code with `synctest.Test`, everything will run inside the bubble — the test code, the `Calc` function we're testing, and its goroutine.

```go
func TestSync(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        Calc()

        // (X)

        got := state.Load()
        if got != 42 {
            t.Errorf("got: %v; want: 42", got)
        }
    })
}
```

At point ⓧ, we want to wait for the `Calc` goroutine to finish. The `synctest.Wait` function comes to the rescue! It blocks the calling goroutine until all other goroutines in the bubble are finished. (It's actually a bit more complicated than that, but we'll talk about it later.)

In our case, there's only one other goroutine (the inner `Calc` goroutine), so `Wait` will pause until it finishes, and then the test will move on.

```go
func TestSync(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        Calc()

        // Wait for the goroutine to finish.
        synctest.Wait()

        got := state.Load()
        if got != 42 {
            t.Errorf("got: %v; want: 42", got)
        }
    })
}
```

```
=== RUN   TestSync
--- PASS: TestSync (0.00s)
```

Now the test passes instantly. That's better!

---

## Checking the Channel State

As we've seen, you can use `synctest.Wait` to wait for the tested goroutine to finish, and then check the state of the data you are interested in. You can also use it to check the state of channels.

Let's say there's a function that generates N numbers like 11, 22, 33, and so on:

```go
// Generate produces n numbers like 11, 22, 33, ...
func Generate(n int) <-chan int {
    out := make(chan int)
    go func() {
        for i := range n {
            out <- (i+1)*10 + (i + 1)
        }
    }()
    return out
}
```

And a simple test:

```go
func Test(t *testing.T) {
    out := Generate(2)
    var got int

    got = <-out
    if got != 11 {
        t.Errorf("#1: got %v, want 11", got)
    }
    got = <-out
    if got != 22 {
        t.Errorf("#1: got %v, want 22", got)
    }
}
```

```
PASS
```

Set N=2, get the first number from the generator's output channel, then get the second number. The test passed, so the function works correctly. But does it really?

Let's use `Generate` in "production":

```go
func main() {
    for v := range Generate(3) {
        fmt.Print(v, " ")
    }
}
```

```
11 22 33 fatal error: all goroutines are asleep - deadlock!
```

Panic! We forgot to close the `out` channel when exiting the inner `Generate` goroutine, so the for-range loop waiting on that channel got stuck.

Let's fix the code:

```go
// Generate produces n numbers like 11, 22, 33, ...
func Generate(n int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for i := range n {
            out <- (i+1)*10 + (i + 1)
        }
    }()
    return out
}
```

And add a test for the `out` channel state:

```go
func Test(t *testing.T) {
    out := Generate(2)
    <-out // 11
    <-out // 22

    // (X)

    // Check that the channel is closed.
    select {
    case _, ok := <-out:
        if ok {
            t.Errorf("expected channel to be closed")
        }
    default:
        t.Errorf("expected channel to be closed")
    }
}
```

```
--- FAIL: Test (0.00s)
    main_test.go:41: expected channel to be closed
```

The test is still failing, even though we're now closing the channel when the `Generate` goroutine exits.

This is a familiar problem: at point ⓧ, we didn't wait for the inner `Generate` goroutine to finish. We need to use `synctest.Wait`:

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        out := Generate(2)
        <-out // 11
        <-out // 22

        // Wait for the goroutine to finish.
        synctest.Wait()

        // Check that the channel is closed.
        select {
        case _, ok := <-out:
            if ok {
                t.Errorf("expected channel to be closed")
            }
        default:
            t.Errorf("expected channel to be closed")
        }
    })
}
```

```
PASS
```

Now the test passes!

---

## Checking for Leaks

Sometimes you want to make sure that a goroutine doesn't leak. For example, if a server starts a goroutine that should stop when the server stops, you want to verify that the goroutine actually stops.

Let's say we have a server that produces consecutive integers:

```go
// IncServer produces consecutive integers starting from 0.
type IncServer struct {
    // ...
}

// Start runs the server in a separate goroutine and
// sends numbers to the out channel.
func (s *IncServer) Start(out chan<- int)

// Stop stops the server.
func (s *IncServer) Stop()
```

We want to test that when we call `Stop`, the server goroutine actually stops. We can do this by checking that the goroutine count doesn't increase after `Stop` is called.

However, with `synctest`, we can do this more elegantly. If a goroutine doesn't stop when it should, it will remain in the bubble, and `synctest.Test` will panic when it finishes:

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        nums := make(chan int)
        server := new(IncServer)
        server.Start(nums)

        // Get some numbers.
        got := [3]int{<-nums, <-nums, <-nums}
        want := [3]int{0, 1, 2}
        if got != want {
            t.Errorf("First 3: got: %v; want: %v", got, want)
        }

        // Stop the server.
        server.Stop()

        // Wait for the server goroutine to finish.
        synctest.Wait()
    })
}
```

If the server goroutine doesn't stop, `synctest.Test` will panic when it tries to finish, indicating a leak.

---

## Durable Blocking

Not all blocking operations are visible to the `synctest` bubble. Only _durable blocks_ are managed by the bubble.

A durable block is a blocking operation that the bubble can detect and control. The following operations are durable blocks:

* A blocking send or receive on a channel created within the bubble.
* A blocking select statement where every case is a channel created within the bubble.
* Calling `Cond.Wait`.
* Calling `WaitGroup.Wait` if all `WaitGroup.Add` calls were made inside the bubble.
* Calling `time.Sleep`.

Operations that are not durable blocks include:

* Blocking on channels created outside the bubble.
* Blocking on system calls.
* Blocking on I/O operations.

---

## Instant Waiting

One of the key features of `synctest` is that it can make time pass instantly when all goroutines are durably blocked. This means that `time.Sleep` calls inside the bubble don't actually take real time — the bubble advances its fake clock to skip over the sleep duration.

For example:

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        start := time.Now()
        time.Sleep(1 * time.Hour)
        elapsed := time.Since(start)
        fmt.Println("Elapsed:", elapsed)
    })
}
```

```
Elapsed: 1h0m0s
```

The test completes instantly, even though we slept for an hour!

---

## Time Inside the Bubble

The bubble uses a fake clock that starts at 2000-01-01 00:00:00 UTC. Time in the bubble only moves forward when all goroutines are durably blocked. The bubble advances time by the smallest amount needed to unblock at least one goroutine.

This means that:

* `time.Now()` returns the current fake time, not real time.
* `time.Sleep` doesn't actually wait — the bubble just advances the clock.
* Timers and tickers work with the fake clock.

---

## Checking for Cleanup

Sometimes you need to verify that cleanup code runs when a test finishes. For example, you might want to check that a server stops properly.

### Using defer

You can use `defer` to ensure cleanup runs:

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        nums := make(chan int)
        server := new(IncServer)
        server.Start(nums)
        defer server.Stop()

        got := [3]int{<-nums, <-nums, <-nums}
        want := [3]int{0, 1, 2}
        if got != want {
            t.Errorf("First 3: got: %v; want: %v", got, want)
        }
    })
}
```

However, `defer` runs when the function returns, which might be before all assertions complete.

### Using t.Cleanup

A better approach is to use `t.Cleanup`, which runs when the test finishes:

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        nums := make(chan int)
        server := new(IncServer)
        server.Start(nums)
        t.Cleanup(server.Stop)

        got := [3]int{<-nums, <-nums, <-nums}
        want := [3]int{0, 1, 2}
        if got != want {
            t.Errorf("First 3: got: %v; want: %v", got, want)
        }
    })
}
```

The `t.Cleanup` approach works because it calls `Stop` when `synctest.Test` has finished — after all the assertions have already run.

### Using t.Context

Sometimes, a context (`context.Context`) is used to stop the server instead of a separate method. In that case, our server interface might look like this:

```go
// IncServer produces consecutive integers starting from 0.
type IncServer struct {
    // ...
}

// Start runs the server in a separate goroutine and
// sends numbers to the out channel until the context is canceled.
func (s *IncServer) Start(ctx context.Context, out chan<- int)
```

Now we don't even need to use `defer` or `t.Cleanup` to check whether the server stops when the context is canceled. Just pass `t.Context()` as the context:

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        nums := make(chan int)
        server := new(IncServer)
        server.Start(t.Context(), nums)

        got := [3]int{<-nums, <-nums, <-nums}
        want := [3]int{0, 1, 2}
        if got != want {
            t.Errorf("First 3: got: %v; want: %v", got, want)
        }
    })
}
```

```
PASS
```

`t.Context()` returns a context that is automatically created when the test starts and is automatically canceled when the test finishes.

Here's how it works:

1. The main test code runs.
2. Before the test finishes, the `t.Context()` context is automatically canceled.
3. The server goroutine stops (as long as the server is implemented correctly and checks for context cancellation).
4. `synctest.Test` sees that there are no blocked goroutines and finishes without panicking.

### Summary

To check for stopping via a method or function, use `defer` or `t.Cleanup()`.

To check for cancellation or stopping via context, use `t.Context()`.

Inside a bubble, `t.Context()` returns a context whose channel is associated with the bubble. The context is automatically canceled when `synctest.Test` ends.

Functions registered with `t.Cleanup()` inside the bubble run just before `synctest.Test` finishes.

---

## Bubble Rules

Let's go over the rules for living in the `synctest` bubble.

### General

* A bubble is created by calling `synctest.Test`. Each call creates a separate bubble.
* Goroutines started inside the bubble become part of it.
* The bubble can only manage durable blocks. Other types of blocks are invisible to it.

### synctest.Test

* If all goroutines in the bubble are durably blocked with no way to unblock them (such as by advancing the clock or returning from a `synctest.Wait` call), `Test` panics.
* When `Test` finishes, it tries to wait for all child goroutines to complete. However, if even a single goroutine is durably blocked, `Test` panics.
* Calling `t.Context()` returns a context whose channel is associated with the bubble.
* Functions registered with `t.Cleanup()` run inside the bubble, immediately before `Test` returns.

### synctest.Wait

* Calling `Wait` in a bubble blocks the goroutine that called it.
* `Wait` returns when all other goroutines in the bubble are durably blocked.
* `Wait` returns when all other goroutines in the bubble have finished.

### Time

* The bubble uses a fake clock (starting at 2000-01-01 00:00:00 UTC).
* Time in the bubble only moves forward if all goroutines are durably blocked.
* Time advances by the smallest amount needed to unblock at least one goroutine.
* If the bubble has to choose between moving time forward or returning from a running `synctest.Wait`, it returns from `Wait`.

The following operations durably block a goroutine:

* A blocking send or receive on a channel created within the bubble.
* A blocking select statement where every case is a channel created within the bubble.
* Calling `Cond.Wait`.
* Calling `WaitGroup.Wait` if all `WaitGroup.Add` calls were made inside the bubble.
* Calling `time.Sleep`.

### Limitations

The `synctest` limitations are quite logical, and you probably won't run into them.

**Don't create channels or objects that contain channels (like tickers or timers) outside the bubble.** Otherwise, the bubble won't be able to manage them, and the test will hang:

```go
func Test(t *testing.T) {
    ch := make(chan int)
    synctest.Test(t, func(t *testing.T) {
        go func() { <-ch }()
        synctest.Wait()
        close(ch)
    })
}
```

```
panic: test timed out after 3s
```

**Don't access synchronization primitives associated with a bubble from outside the bubble:**

```go
func Test(t *testing.T) {
    var ch chan int
    synctest.Test(t, func(t *testing.T) {
        ch = make(chan int)
    })
    close(ch)
}
```

```
panic: close of synctest channel from outside bubble
```

**Don't call `T.Run`, `T.Parallel`, or `T.Deadline` inside a bubble:**

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        t.Run("subtest", func(t *testing.T) {
            t.Log("ok")
        })
    })
}
```

```
panic: testing: t.Run called inside synctest bubble
```

**Don't call `synctest.Test` inside the bubble:**

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        synctest.Test(t, func(t *testing.T) {
            t.Log("ok")
        })
    })
}
```

```
panic: synctest.Run called from within a synctest bubble
```

**Don't call `synctest.Wait` from outside the bubble:**

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        t.Log("ok")
    })
    synctest.Wait()
}
```

```
panic: goroutine is not in a bubble [recovered, repanicked]
```

**Don't call `synctest.Wait` concurrently from multiple goroutines:**

```go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        go synctest.Wait()
        go synctest.Wait()
    })
}
```

```
panic: wait already in progress
```

---

## Summary

The `synctest` package is a complicated beast. But now that you've studied it, you can test concurrent programs no matter what synchronization tools they use—channels, selects, wait groups, timers or tickers, or even `time.Sleep`.

Key points to remember:

* **Use `synctest.Test`** to create an isolated testing bubble for concurrent code.
* **Use `synctest.Wait`** to wait for goroutines to finish instead of `time.Sleep`.
* **Durable blocks** are managed by the bubble; other blocks are not.
* **Time in the bubble** is fake and advances instantly when all goroutines are blocked.
* **Use `t.Cleanup()`** or `t.Context()`** to verify cleanup code runs properly.
* **Follow the bubble rules** to avoid panics and test failures.

With `synctest`, you can write fast, reliable tests for concurrent Go code without relying on arbitrary delays or complex synchronization logic.


