Skip to content
Draft
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a93c50b
docs(README.md): add testing/synctest guidance
mafredri Nov 3, 2026
d387b5c
docs(README.md): state the outer-t synctest crash as a rule
mafredri Nov 3, 2026
cd6ca26
docs(README.md): warn that bubble time uses up test timeouts
mafredri Nov 3, 2026
30d1aed
docs(README.md): point HTTP tests at httptest.NewTestServer
mafredri Nov 3, 2026
6548c2a
docs(README.md): list durable blocking with the WaitGroup limits
mafredri Nov 3, 2026
8a3c9f3
docs(README.md): describe the expired timeout, not one failure
mafredri Nov 3, 2026
526d901
docs(README.md): create httptest.NewTestServer inside the bubble
mafredri Nov 3, 2026
313cd28
docs(README.md): rewrite the synctest section around the choice
mafredri Nov 3, 2026
1d31160
docs(README.md): scope the bubble test-timeout advice
mafredri Nov 3, 2026
be2ceea
docs(README.md): state the synctest condition as no real I/O
mafredri Nov 3, 2026
c15e02b
docs(README.md): tie httptest.NewTestServer to the go.mod version
mafredri Nov 3, 2026
2c1aba2
docs(README.md): say to keep the production NewReal() in a bubble
mafredri Nov 3, 2026
3332282
docs(README.md): say what stopped bubble time does to timeouts
mafredri Nov 3, 2026
72aea50
docs(README.md): name what moves a *Mock in a bubble
mafredri Nov 3, 2026
2f09d5f
docs(README.md): explain synctest.Wait in the NewReal() example
mafredri Nov 3, 2026
a499aee
docs(README.md): show a test timeout expiring during time.Sleep
mafredri Nov 3, 2026
3c4bd18
docs(README.md): limit the free-timeout claim to durable blocking
mafredri Nov 3, 2026
9aed433
docs(README.md): tighten the synctest section after the audit fixes
mafredri Nov 3, 2026
c0deeb4
docs(README.md): warn that network waits in a bubble hang the test
mafredri Nov 4, 2026
8630169
docs(README.md): say synctest.Wait can replace waiting-only traps
mafredri Nov 4, 2026
32ca6c2
docs(README.md): quote only the common part of the outer-t crash
mafredri Nov 4, 2026
1552e68
docs(README.md): say when synctest.Wait can stand in for a trap
mafredri Nov 4, 2026
2a38969
docs(README.md): state the I/O wait rule once in the synctest section
mafredri Nov 4, 2026
00497a3
docs(README.md): say bubble time jumps to the earliest pending timer
mafredri Nov 4, 2026
8fc031e
docs(README.md): tie synctest to go.mod and place the Mock bullet
mafredri Nov 4, 2026
23344f0
docs(README.md): shorten the synctest.Wait bullet
mafredri Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,56 @@ func (c *Component) Method() {
This makes it much less likely that code changes that introduce new components or methods will spoil
existing unit tests.

## Quartz and `testing/synctest`

With `go 1.25` or later in `go.mod`, [`synctest.Test`](https://pkg.go.dev/testing/synctest) runs a
test in a "bubble" whose fake clock moves forward when every goroutine in the bubble is [durably
blocked](https://pkg.go.dev/testing/synctest#hdr-Blocking). To test time-dependent code:

- If the test does no real I/O, run it in `synctest.Test` and keep the production
`quartz.NewReal()`. Inside a bubble it runs on bubble time, so you need no `*Mock`, `Advance()`,
or traps.
- If the test does real I/O, such as database queries or real network requests, use a `*Mock`
outside a bubble, as in [Mocking](#mocking). While a goroutine waits on I/O, bubble time does not
move and `synctest.Wait()` does not return, so a timeout does not stop a slow query or request.
With `go 1.27` or later in `go.mod`, HTTP tests can stay in the bubble with an
`httptest.NewTestServer` created there and its `Client()`.
- If you want to decide when time moves, or need traps, use a `*Mock`, inside or outside a bubble.
It ignores bubble time; only `Advance()`, `AdvanceNext()`, and `Set()` move it.

```go
func TestTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
clk := quartz.NewReal()
ctx, cancel := clk.WithTimeout(t.Context(), time.Minute)
defer cancel()
time.Sleep(time.Minute) // moves bubble time; no wall-clock wait
synctest.Wait() // let the context's timer callback mark ctx done
if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Fatal("expected deadline exceeded")
}
})
}
```

Inside a bubble:

- Do not create an `httptest.NewServer` or leave open a connection that a goroutine reads from, such
as an HTTP client's idle keep-alive connection. That goroutine waits on the network, which stops
bubble time and `synctest.Wait()` as described above, so the test hangs at its next `time.Sleep`,
timeout, or `synctest.Wait()`, and `synctest.Test` does not return until that goroutine exits.
- Create a `*Mock` with the `t` that `synctest.Test` passes in. Cleanup on the outer test's `t` runs
outside the bubble and can crash the test binary with a fatal error about a
`synctest channel from outside bubble`.
- With a `*Mock`, `synctest.Wait()` can replace a trap whose only job is to wait until the code has
made a clock call, such as `NewTimer`, if the code makes that call before it first blocks. If the
code blocks first, for example on a channel or in `time.Sleep`, `synctest.Wait()` returns before
the call and a following `Advance()` misses the timer; keep the trap.
- Test timeouts created in a bubble, such as the `ctx` you pass to `MustWait`, run on bubble time.
Make them longer than the bubble time that passes while they are in use: a 10-second `ctx` expires
during a `time.Sleep(time.Minute)`. A long timeout costs no wall time: when every goroutine in the
bubble is durably blocked, bubble time jumps to the earliest pending timer.

## Why another time testing library?

Writing good unit tests for components and functions that use the `time` package is difficult, even
Expand Down