Skip to content

Commit 82e85b4

Browse files
committed
chore: add usage information to clock library README
1 parent 02ffff1 commit 82e85b4

File tree

1 file changed

+357
-7
lines changed

1 file changed

+357
-7
lines changed

clock/README.md

Lines changed: 357 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,362 @@ A Go time testing library for writing deterministic unit tests
55
_Note: Quartz is the name I'm targeting for the standalone open source project when we spin this
66
out._
77

8+
Our high level goal is to write unit tests that
9+
10+
1. execute quickly
11+
2. don't flake
12+
3. are straightforward to write and understand
13+
14+
For tests to execute quickly without flakes, we want to focus on _determinism_: the test should run
15+
the same each time, and it should be easy to force the system into a known state (no races) before
16+
executing test assertions. `time.Sleep`, `runtime.Gosched()`, and
17+
polling/[Eventually](https://pkg.go.dev/github.com/stretchr/testify/assert#Eventually) are all
18+
symptoms of an inability to do this easily.
19+
20+
## Usage
21+
22+
### `Clock` interface
23+
24+
In your application code, maintain a reference to a `quartz.Clock` instance to start timers and
25+
tickers, instead of the bare `time` standard library.
26+
27+
```go
28+
import "github.com/coder/quartz"
29+
30+
type Component struct {
31+
...
32+
33+
// for testing
34+
clock quartz.Clock
35+
}
36+
```
37+
38+
Whenever you would call into `time` to start a timer or ticker, call `Component.clock` instead.
39+
40+
In production, set this clock to `quartz.NewReal()` to create a clock that just transparently passes
41+
through to the realtime clock.
42+
43+
### Mocking
44+
45+
In your tests, you can use a `*Mock` to control the tickers and timers your code under test gets.
46+
47+
```go
48+
import (
49+
"testing"
50+
"github.com/coder/quartz"
51+
)
52+
53+
func TestComponent(t *testing.T) {
54+
mClock := quartz.NewMock(t)
55+
comp := &Component{
56+
...
57+
clock: mClock,
58+
}
59+
}
60+
```
61+
62+
The `*Mock` clock starts at Jan 1, 2024, 00:00 UTC by default, but you can set any start time you'd like prior to your test.
63+
64+
```go
65+
mClock := quartz.NewMock(t)
66+
mClock.Set(time.Date(2021, 6, 18, 12, 0, 0, 0, time.UTC)) // June 18, 2021 @ 12pm UTC
67+
```
68+
69+
#### Advancing the clock
70+
71+
Once you begin setting timers or tickers, you cannot change the time backward, only advance it
72+
forward. You may continue to use `Set()`, but it is often easier and clearer to use `Advance()`.
73+
74+
For example, with a timer:
75+
76+
```go
77+
fired := false
78+
79+
tmr := mClock.Afterfunc(time.Second, func() {
80+
fired = true
81+
})
82+
mClock.Advance(time.Second)
83+
```
84+
85+
When you call `Advance()` it immediately moves the clock forward the given amount, and triggers any
86+
tickers or timers that are scheduled to happen at that time. Any triggered events happen on separate
87+
goroutines, so _do not_ immediately assert the results:
88+
89+
```go
90+
fired := false
91+
92+
tmr := mClock.Afterfunc(time.Second, func() {
93+
fired = true
94+
})
95+
mClock.Advance(time.Second)
96+
97+
// RACE CONDITION, DO NOT DO THIS!
98+
if !fired {
99+
t.Fatal("didn't fire")
100+
}
101+
```
102+
103+
`Advance()` (and `Set()` for that matter) return an `AdvanceWaiter` object you can use to wait for
104+
all triggered events to complete.
105+
106+
```go
107+
fired := false
108+
// set a test timeout so we don't wait the default `go test` timeout for a failure
109+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
110+
111+
tmr := mClock.Afterfunc(time.Second, func() {
112+
fired = true
113+
})
114+
115+
w := mClock.Advance(time.Second)
116+
err := w.Wait(ctx)
117+
if err != nil {
118+
t.Fatal("AfterFunc f never completed")
119+
}
120+
if !fired {
121+
t.Fatal("didn't fire")
122+
}
123+
```
124+
125+
The construction of waiting for the triggered events and failing the test if they don't complete is
126+
very common, so there is a shorthand:
127+
128+
```go
129+
w := mClock.Advance(time.Second)
130+
err := w.Wait(ctx)
131+
if err != nil {
132+
t.Fatal("AfterFunc f never completed")
133+
}
134+
```
135+
136+
is equivalent to:
137+
138+
```go
139+
w := mClock.Advance(time.Second)
140+
w.MustWait(ctx)
141+
```
142+
143+
or even more briefly:
144+
145+
```go
146+
mClock.Advance(time.Second).MustWait(ctx)
147+
```
148+
149+
### Advance only to the next event
150+
151+
One important restriction on advancing the clock is that you may only advance forward to the next
152+
timer or ticker event and no further. The following will result in a test failure:
153+
154+
```go
155+
func TestAdvanceTooFar(t *testing.T) {
156+
ctx, cancel := context.WithTimeout(10*time.Second)
157+
defer cancel()
158+
mClock := quartz.NewMock(t)
159+
var firedAt time.Time
160+
mClock.AfterFunc(time.Second, func() {
161+
firedAt := mClock.Now()
162+
})
163+
mClock.Advance(2*time.Second).MustWait(ctx)
164+
}
165+
```
166+
167+
This is a deliberate design decision to allow `Advance()` to immediately and synchronously move the
168+
clock forward (even without calling `Wait()` on returned waiter). This helps meet Quartz's design
169+
goals of writing deterministic and easy to understand unit tests. It also allows the clock to be
170+
advanced, deterministically _during_ the execution of a tick or timer function, as explained in the
171+
next sections on Traps.
172+
173+
Advancing multiple events can be accomplished via looping. E.g. if you have a 1-second ticker
174+
175+
```go
176+
for i:=0; i<10; i++ {
177+
mClock.Advance(time.Second).MustWait(ctx)
178+
}
179+
```
180+
181+
will advance 10 ticks.
182+
183+
If you don't know or don't want to compute the time to the next event, you can use `AdvanceNext()`.
184+
185+
```go
186+
d, w := mClock.AdvanceNext()
187+
w.MustWait(ctx)
188+
// d contains the duration we advanced
189+
```
190+
191+
### Traps
192+
193+
A trap allows you to match specific calls into the library while mocking, block their return,
194+
inspect their arguments, then release them to allow them to return. They help you write
195+
deterministic unit tests even when the code under test executes asynchronously from the test.
196+
197+
You set your traps prior to executing code under test, and then wait for them to be triggered.
198+
199+
```go
200+
func TestTrap(t *testing.T) {
201+
ctx, cancel := context.WithTimeout(10*time.Second)
202+
defer cancel()
203+
mClock := quartz.NewMock(t)
204+
trap := mClock.Trap().AfterFunc()
205+
defer trap.Close() // stop trapping AfterFunc calls
206+
207+
count := 0
208+
go mClock.AfterFunc(time.Hour, func(){
209+
count++
210+
})
211+
call := trap.MustWait(ctx)
212+
call.Release()
213+
if call.Duration != time.Hour {
214+
t.Fatal("wrong duration")
215+
}
216+
217+
// Now that the async call to AfterFunc has occurred, we can advance the clock to trigger it
218+
mClock.Advance(time.Hour).MustWait(ctx)
219+
if count != 1 {
220+
t.Fatal("wrong count")
221+
}
222+
}
223+
```
224+
225+
In this test, the trap serves 2 purposes. Firstly, it allows us to capture and assert the duration
226+
passed to the `AfterFunc` call. Secondly, it prevents a race between setting the timer and advancing
227+
it. Since these things happen on different goroutines, if `Advance()` completes before
228+
`AfterFunc()` is called, then the timer never pops in this test.
229+
230+
Any untrapped calls immediately complete using the current time, and calling `Close()` on a trap
231+
causes the mock clock to stop trapping those calls.
232+
233+
You may also `Advance()` the clock between trapping a call and releasing it. The call uses the
234+
current (mocked) time at the moment it is released.
235+
236+
```go
237+
func TestTrap2(t *testing.T) {
238+
ctx, cancel := context.WithTimeout(10*time.Second)
239+
defer cancel()
240+
mClock := quartz.NewMock(t)
241+
trap := mClock.Trap().Since()
242+
defer trap.Close() // stop trapping AfterFunc calls
243+
244+
var logs []string
245+
done := make(chan struct{})
246+
go func(clk quartz.Clock){
247+
defer close(done)
248+
start := clk.Now()
249+
phase1()
250+
p1end := clk.Now()
251+
logs = append(fmt.Sprintf("Phase 1 took %s", p1end.Sub(start).String()))
252+
phase2()
253+
p2end := clk.Now()
254+
logs = append(fmt.Sprintf("Phase 2 took %s", p2end.Sub(p1end).String()))
255+
}(mClock)
256+
257+
// start
258+
trap.MustWait(ctx).Release()
259+
// phase 1
260+
call := trap.MustWait(ctx)
261+
mClock.Advance(3*time.Second).MustWait(ctx)
262+
call.Release()
263+
// phase 2
264+
call = trap.MustWait(ctx)
265+
mClock.Advance(5*time.Second).MustWait(ctx)
266+
call.Release()
267+
268+
<-done
269+
// Now logs contains []string{"Phase 1 took 3s", "Phase 2 took 5s"}
270+
}
271+
```
272+
273+
### Tags
274+
275+
When multiple goroutines in the code under test call into the Clock, you can use `tags` to
276+
distinguish them in your traps.
277+
278+
```go
279+
trap := mClock.Trap.Now("foo") // traps any calls that contain "foo"
280+
defer trap.Close()
281+
282+
foo := make(chan time.Time)
283+
go func(){
284+
foo <- mClock.Now("foo", "bar")
285+
}()
286+
baz := make(chan time.Time)
287+
go func(){
288+
baz <- mClock.Now("baz")
289+
}()
290+
call := trap.MustWait(ctx)
291+
mClock.Advance(time.Second).MustWait(ctx)
292+
call.Release()
293+
// call.Tags contains []string{"foo", "bar"}
294+
295+
gotFoo := <-foo // 1s after start
296+
gotBaz := <-baz // ?? never trapped, so races with Advance()
297+
```
298+
299+
Tags appear as an optional suffix on all `Clock` methods (type `...string`) and are ignored entirely
300+
by the realtime clock. They also appear on all methods on returned timers and tickers.
301+
302+
## Recommended Patterns
303+
304+
### Options
305+
306+
We use the Option pattern to inject the mock clock for testing, keeping the call signature in
307+
production clean. The option pattern is compatible with other optional fields as well.
308+
309+
```go
310+
type Option func(*Thing)
311+
312+
// WithTestClock is used in tests to inject a mock Clock
313+
func WithTestClock(clk quartz.Clock) Option {
314+
return func(t *Thing) {
315+
t.clock = clk
316+
}
317+
}
318+
319+
func NewThing(<required args>, opts ...Option) *Thing {
320+
t := &Thing{
321+
...
322+
clock: quartz.NewReal()
323+
}
324+
for _, o := range opts {
325+
o(t)
326+
}
327+
return t
328+
}
329+
```
330+
331+
In tests, this becomes
332+
333+
```go
334+
func TestThing(t *testing.T) {
335+
mClock := quartz.NewMock(t)
336+
thing := NewThing(<required args>, WithTestClock(mClock))
337+
...
338+
}
339+
```
340+
341+
### Tagging convention
342+
343+
Tag your `Clock` method calls as:
344+
345+
```go
346+
func (c *Component) Method() {
347+
now := c.clock.Now("Component", "Method")
348+
}
349+
```
350+
351+
or
352+
353+
```go
354+
func (c *Component) Method() {
355+
start := c.clock.Now("Component", "Method", "start")
356+
...
357+
end := c.clock.Now("Component", "Method", "end")
358+
}
359+
```
360+
361+
This makes it much less likely that code changes that introduce new components or methods will spoil
362+
existing unit tests.
363+
8364
## Why another time testing library?
9365

10366
Writing good unit tests for components and functions that use the `time` package is difficult, even
@@ -18,7 +374,7 @@ Quartz shares the high level design of a `Clock` interface that closely resemble
18374
the `time` standard library, and a "real" clock passes thru to the standard library in production,
19375
while a mock clock gives precise control in testing.
20376

21-
Our high level goal is to write unit tests that
377+
As mentioned in our introduction, our high level goal is to write unit tests that
22378

23379
1. execute quickly
24380
2. don't flake
@@ -27,12 +383,6 @@ Our high level goal is to write unit tests that
27383
For several reasons, this is a tall order when it comes to code that depends on time, and we found
28384
the existing libraries insufficient for our goals.
29385

30-
For tests to execute quickly without flakes, we want to focus on _determinism_: the test should run
31-
the same each time, and it should be easy to force the system into a known state (no races) before
32-
executing test assertions. `time.Sleep`, `runtime.Gosched()`, and
33-
polling/[Eventually](https://pkg.go.dev/github.com/stretchr/testify/assert#Eventually) are all
34-
symptoms of an inability to do this easily.
35-
36386
### Preventing test flakes
37387

38388
The following example comes from the README from benbjohnson/clock:

0 commit comments

Comments
 (0)