@@ -5,6 +5,362 @@ A Go time testing library for writing deterministic unit tests
5
5
_ Note: Quartz is the name I'm targeting for the standalone open source project when we spin this
6
6
out._
7
7
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 <- time.Now (" foo" , " bar" )
285
+ }()
286
+ baz := make (chan time.Time )
287
+ go func (){
288
+ baz <- time.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
+
8
364
## Why another time testing library?
9
365
10
366
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
18
374
the ` time ` standard library, and a "real" clock passes thru to the standard library in production,
19
375
while a mock clock gives precise control in testing.
20
376
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
22
378
23
379
1 . execute quickly
24
380
2 . don't flake
@@ -27,12 +383,6 @@ Our high level goal is to write unit tests that
27
383
For several reasons, this is a tall order when it comes to code that depends on time, and we found
28
384
the existing libraries insufficient for our goals.
29
385
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
-
36
386
### Preventing test flakes
37
387
38
388
The following example comes from the README from benbjohnson/clock:
0 commit comments