Skip to content

Commit fcecc22

Browse files
committed
chore: add usage information to clock library README
1 parent 39aa737 commit fcecc22

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy