{"id":18581910,"url":"https://github.com/coder/quartz","last_synced_at":"2025-04-13T00:48:28.234Z","repository":{"id":246835268,"uuid":"822540108","full_name":"coder/quartz","owner":"coder","description":"A Go time testing library for writing deterministic unit tests","archived":false,"fork":false,"pushed_at":"2025-01-02T04:37:00.000Z","size":33,"stargazers_count":255,"open_issues_count":0,"forks_count":4,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-04-13T00:48:19.376Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"cc0-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/coder.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-01T10:38:57.000Z","updated_at":"2025-04-09T01:27:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"b68b816b-da32-417b-8d6e-bfa1c2ec6379","html_url":"https://github.com/coder/quartz","commit_stats":null,"previous_names":["coder/quartz"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coder%2Fquartz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coder%2Fquartz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coder%2Fquartz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coder%2Fquartz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/coder","download_url":"https://codeload.github.com/coder/quartz/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248650437,"owners_count":21139672,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-07T00:08:19.009Z","updated_at":"2025-04-13T00:48:28.210Z","avatar_url":"https://github.com/coder.png","language":"Go","readme":"# Quartz\n\nA Go time testing library for writing deterministic unit tests\n\nOur high level goal is to write unit tests that\n\n1. execute quickly\n2. don't flake\n3. are straightforward to write and understand\n\nFor tests to execute quickly without flakes, we want to focus on _determinism_: the test should run\nthe same each time, and it should be easy to force the system into a known state (no races) before\nexecuting test assertions. `time.Sleep`, `runtime.Gosched()`, and\npolling/[Eventually](https://pkg.go.dev/github.com/stretchr/testify/assert#Eventually) are all\nsymptoms of an inability to do this easily.\n\n## Usage\n\n### `Clock` interface\n\nIn your application code, maintain a reference to a `quartz.Clock` instance to start timers and\ntickers, instead of the bare `time` standard library.\n\n```go\nimport \"github.com/coder/quartz\"\n\ntype Component struct {\n\t...\n\n\t// for testing\n\tclock quartz.Clock\n}\n```\n\nWhenever you would call into `time` to start a timer or ticker, call `Component`'s `clock` instead.\n\nIn production, set this clock to `quartz.NewReal()` to create a clock that just transparently passes\nthrough to the standard `time` library.\n\n### Mocking\n\nIn your tests, you can use a `*Mock` to control the tickers and timers your code under test gets.\n\n```go\nimport (\n\t\"testing\"\n\t\"github.com/coder/quartz\"\n)\n\nfunc TestComponent(t *testing.T) {\n\tmClock := quartz.NewMock(t)\n\tcomp := \u0026Component{\n\t\t...\n\t\tclock: mClock,\n\t}\n}\n```\n\nThe `*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.\n\n```go\nmClock := quartz.NewMock(t)\nmClock.Set(time.Date(2021, 6, 18, 12, 0, 0, 0, time.UTC)) // June 18, 2021 @ 12pm UTC\n```\n\n#### Advancing the clock\n\nOnce you begin setting timers or tickers, you cannot change the time backward, only advance it\nforward. You may continue to use `Set()`, but it is often easier and clearer to use `Advance()`.\n\nFor example, with a timer:\n\n```go\nfired := false\n\ntmr := mClock.AfterFunc(time.Second, func() {\n  fired = true\n})\nmClock.Advance(time.Second)\n```\n\nWhen you call `Advance()` it immediately moves the clock forward the given amount, and triggers any\ntickers or timers that are scheduled to happen at that time. Any triggered events happen on separate\ngoroutines, so _do not_ immediately assert the results:\n\n```go\nfired := false\n\ntmr := mClock.AfterFunc(time.Second, func() {\n  fired = true\n})\nmClock.Advance(time.Second)\n\n// RACE CONDITION, DO NOT DO THIS!\nif !fired {\n  t.Fatal(\"didn't fire\")\n}\n```\n\n`Advance()` (and `Set()` for that matter) return an `AdvanceWaiter` object you can use to wait for\nall triggered events to complete.\n\n```go\nfired := false\n// set a test timeout so we don't wait the default `go test` timeout for a failure\nctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\ntmr := mClock.AfterFunc(time.Second, func() {\n  fired = true\n})\n\nw := mClock.Advance(time.Second)\nerr := w.Wait(ctx)\nif err != nil {\n  t.Fatal(\"AfterFunc f never completed\")\n}\nif !fired {\n  t.Fatal(\"didn't fire\")\n}\n```\n\nThe construction of waiting for the triggered events and failing the test if they don't complete is\nvery common, so there is a shorthand:\n\n```go\nw := mClock.Advance(time.Second)\nerr := w.Wait(ctx)\nif err != nil {\n  t.Fatal(\"AfterFunc f never completed\")\n}\n```\n\nis equivalent to:\n\n```go\nw := mClock.Advance(time.Second)\nw.MustWait(ctx)\n```\n\nor even more briefly:\n\n```go\nmClock.Advance(time.Second).MustWait(ctx)\n```\n\n### Advance only to the next event\n\nOne important restriction on advancing the clock is that you may only advance forward to the next\ntimer or ticker event and no further. The following will result in a test failure:\n\n```go\nfunc TestAdvanceTooFar(t *testing.T) {\n\tctx, cancel := context.WithTimeout(10*time.Second)\n\tdefer cancel()\n\tmClock := quartz.NewMock(t)\n\tvar firedAt time.Time\n\tmClock.AfterFunc(time.Second, func() {\n\t\tfiredAt := mClock.Now()\n\t})\n\tmClock.Advance(2*time.Second).MustWait(ctx)\n}\n```\n\nThis is a deliberate design decision to allow `Advance()` to immediately and synchronously move the\nclock forward (even without calling `Wait()` on returned waiter). This helps meet Quartz's design\ngoals of writing deterministic and easy to understand unit tests. It also allows the clock to be\nadvanced, deterministically _during_ the execution of a tick or timer function, as explained in the\nnext sections on Traps.\n\nAdvancing multiple events can be accomplished via looping. E.g. if you have a 1-second ticker\n\n```go\nfor i := 0; i \u003c 10; i++ {\n\tmClock.Advance(time.Second).MustWait(ctx)\n}\n```\n\nwill advance 10 ticks.\n\nIf you don't know or don't want to compute the time to the next event, you can use `AdvanceNext()`.\n\n```go\nd, w := mClock.AdvanceNext()\nw.MustWait(ctx)\n// d contains the duration we advanced\n```\n\n`d, ok := Peek()` returns the duration until the next event, if any (`ok` is `true`). You can use\nthis to advance a specific time, regardless of the tickers and timer events:\n\n```go\ndesired := time.Minute // time to advance\nfor desired \u003e 0 {\n\tp, ok := mClock.Peek()\n\tif !ok || p \u003e desired {\n\t\tmClock.Advance(desired).MustWait(ctx)\n\t\tbreak\n\t}\n\tmClock.Advance(p).MustWait(ctx)\n\tdesired -= p\n}\n```\n\n### Traps\n\nA trap allows you to match specific calls into the library while mocking, block their return,\ninspect their arguments, then release them to allow them to return. They help you write\ndeterministic unit tests even when the code under test executes asynchronously from the test.\n\nYou set your traps prior to executing code under test, and then wait for them to be triggered.\n\n```go\nfunc TestTrap(t *testing.T) {\n\tctx, cancel := context.WithTimeout(10*time.Second)\n\tdefer cancel()\n\tmClock := quartz.NewMock(t)\n\ttrap := mClock.Trap().AfterFunc()\n\tdefer trap.Close() // stop trapping AfterFunc calls\n\n\tcount := 0\n\tgo mClock.AfterFunc(time.Hour, func(){\n\t\tcount++\n\t})\n\tcall := trap.MustWait(ctx)\n\tcall.Release()\n\tif call.Duration != time.Hour {\n\t\tt.Fatal(\"wrong duration\")\n\t}\n\n\t// Now that the async call to AfterFunc has occurred, we can advance the clock to trigger it\n\tmClock.Advance(call.Duration).MustWait(ctx)\n\tif count != 1 {\n\t\tt.Fatal(\"wrong count\")\n\t}\n}\n```\n\nIn this test, the trap serves 2 purposes. Firstly, it allows us to capture and assert the duration\npassed to the `AfterFunc` call. Secondly, it prevents a race between setting the timer and advancing\nit. Since these things happen on different goroutines, if `Advance()` completes before\n`AfterFunc()` is called, then the timer never pops in this test.\n\nAny untrapped calls immediately complete using the current time, and calling `Close()` on a trap\ncauses the mock clock to stop trapping those calls.\n\nYou may also `Advance()` the clock between trapping a call and releasing it. The call uses the\ncurrent (mocked) time at the moment it is released.\n\n```go\nfunc TestTrap2(t *testing.T) {\n\tctx, cancel := context.WithTimeout(10*time.Second)\n\tdefer cancel()\n\tmClock := quartz.NewMock(t)\n\ttrap := mClock.Trap().Now()\n\tdefer trap.Close() // stop trapping AfterFunc calls\n\n\tvar logs []string\n\tdone := make(chan struct{})\n\tgo func(clk quartz.Clock){\n\t\tdefer close(done)\n\t\tstart := clk.Now()\n\t\tphase1()\n\t\tp1end := clk.Now()\n\t\tlogs = append(fmt.Sprintf(\"Phase 1 took %s\", p1end.Sub(start).String()))\n\t\tphase2()\n\t\tp2end := clk.Now()\n\t\tlogs = append(fmt.Sprintf(\"Phase 2 took %s\", p2end.Sub(p1end).String()))\n\t}(mClock)\n\n\t// start\n\ttrap.MustWait(ctx).Release()\n\t// phase 1\n\tcall := trap.MustWait(ctx)\n\tmClock.Advance(3*time.Second).MustWait(ctx)\n\tcall.Release()\n\t// phase 2\n\tcall = trap.MustWait(ctx)\n\tmClock.Advance(5*time.Second).MustWait(ctx)\n\tcall.Release()\n\n\t\u003c-done\n\t// Now logs contains []string{\"Phase 1 took 3s\", \"Phase 2 took 5s\"}\n}\n```\n\n### Tags\n\nWhen multiple goroutines in the code under test call into the Clock, you can use `tags` to\ndistinguish them in your traps.\n\n```go\ntrap := mClock.Trap.Now(\"foo\") // traps any calls that contain \"foo\"\ndefer trap.Close()\n\nfoo := make(chan time.Time)\ngo func(){\n\tfoo \u003c- mClock.Now(\"foo\", \"bar\")\n}()\nbaz := make(chan time.Time)\ngo func(){\n\tbaz \u003c- mClock.Now(\"baz\")\n}()\ncall := trap.MustWait(ctx)\nmClock.Advance(time.Second).MustWait(ctx)\ncall.Release()\n// call.Tags contains []string{\"foo\", \"bar\"}\n\ngotFoo := \u003c-foo // 1s after start\ngotBaz := \u003c-baz // ?? never trapped, so races with Advance()\n```\n\nTags appear as an optional suffix on all `Clock` methods (type `...string`) and are ignored entirely\nby the real clock. They also appear on all methods on returned timers and tickers.\n\n## Recommended Patterns\n\n### Options\n\nWe use the Option pattern to inject the mock clock for testing, keeping the call signature in\nproduction clean. The option pattern is compatible with other optional fields as well.\n\n```go\ntype Option func(*Thing)\n\n// WithTestClock is used in tests to inject a mock Clock\nfunc WithTestClock(clk quartz.Clock) Option {\n\treturn func(t *Thing) {\n\t\tt.clock = clk\n\t}\n}\n\nfunc NewThing(\u003crequired args\u003e, opts ...Option) *Thing {\n\tt := \u0026Thing{\n\t\t...\n\t\tclock: quartz.NewReal()\n\t}\n\tfor _, o := range opts {\n\t  o(t)\n\t}\n\treturn t\n}\n```\n\nIn tests, this becomes\n\n```go\nfunc TestThing(t *testing.T) {\n\tmClock := quartz.NewMock(t)\n\tthing := NewThing(\u003crequired args\u003e, WithTestClock(mClock))\n\t...\n}\n```\n\n### Tagging convention\n\nTag your `Clock` method calls as:\n\n```go\nfunc (c *Component) Method() {\n\tnow := c.clock.Now(\"Component\", \"Method\")\n}\n```\n\nor\n\n```go\nfunc (c *Component) Method() {\n\tstart := c.clock.Now(\"Component\", \"Method\", \"start\")\n\t...\n\tend := c.clock.Now(\"Component\", \"Method\", \"end\")\n}\n```\n\nThis makes it much less likely that code changes that introduce new components or methods will spoil\nexisting unit tests.\n\n## Why another time testing library?\n\nWriting good unit tests for components and functions that use the `time` package is difficult, even\nthough several open source libraries exist. In building Quartz, we took some inspiration from\n\n- [github.com/benbjohnson/clock](https://github.com/benbjohnson/clock)\n- Tailscale's [tstest.Clock](https://github.com/coder/tailscale/blob/main/tstest/clock.go)\n- [github.com/aspenmesh/tock](https://github.com/aspenmesh/tock)\n\nQuartz shares the high level design of a `Clock` interface that closely resembles the functions in\nthe `time` standard library, and a \"real\" clock passes thru to the standard library in production,\nwhile a mock clock gives precise control in testing.\n\nAs mentioned in our introduction, our high level goal is to write unit tests that\n\n1. execute quickly\n2. don't flake\n3. are straightforward to write and understand\n\nFor several reasons, this is a tall order when it comes to code that depends on time, and we found\nthe existing libraries insufficient for our goals.\n\n### Preventing test flakes\n\nThe following example comes from the README from benbjohnson/clock:\n\n```go\nmock := clock.NewMock()\ncount := 0\n\n// Kick off a timer to increment every 1 mock second.\ngo func() {\n\tticker := mock.Ticker(1 * time.Second)\n\tfor {\n\t\t\u003c-ticker.C\n\t\tcount++\n\t}\n}()\nruntime.Gosched()\n\n// Move the clock forward 10 seconds.\nmock.Add(10 * time.Second)\n\n// This prints 10.\nfmt.Println(count)\n```\n\nThe first race condition is fairly obvious: moving the clock forward 10 seconds may generate 10\nticks on the `ticker.C` channel, but there is no guarantee that `count++` executes before\n`fmt.Println(count)`.\n\nThe second race condition is more subtle, but `runtime.Gosched()` is the tell. Since the ticker\nis started on a separate goroutine, there is no guarantee that `mock.Ticker()` executes before\n`mock.Add()`. `runtime.Gosched()` is an attempt to get this to happen, but it makes no hard\npromises. On a busy system, especially when running tests in parallel, this can flake, advance the\ntime 10 seconds first, then start the ticker and never generate a tick.\n\nLet's talk about how Quartz tackles these problems.\n\nIn our experience, an extremely common use case is creating a ticker then doing a 2-arm `select`\nwith ticks in one and context expiring in another, i.e.\n\n```go\nt := time.NewTicker(duration)\nfor {\n\tselect {\n\tcase \u003c-ctx.Done():\n\t\treturn ctx.Err()\n\tcase \u003c-t.C:\n\t\terr := do()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n```\n\nIn Quartz, we refactor this to be more compact and testing friendly:\n\n```go\nt := clock.TickerFunc(ctx, duration, do)\nreturn t.Wait()\n```\n\nThis affords the mock `Clock` the ability to explicitly know when processing of a tick is finished\nbecause it's wrapped in the function passed to `TickerFunc` (`do()` in this example).\n\nIn Quartz, when you advance the clock, you are returned an object you can `Wait()` on to ensure all\nticks and timers triggered are finished. This solves the first race condition in the example.\n\n(As an aside, we still support a traditional standard library-style `Ticker`. You may find it useful\nif you want to keep your code as close as possible to the standard library, or if you need to use\nthe channel in a larger `select` block. In that case, you'll have to find some other mechanism to\nsync tick processing to your test code.)\n\nTo prevent race conditions related to the starting of the ticker, Quartz allows you to set \"traps\"\nfor calls that access the clock.\n\n```go\nfunc TestTicker(t *testing.T) {\n\tmClock := quartz.NewMock(t)\n\ttrap := mClock.Trap().TickerFunc()\n\tdefer trap.Close() // stop trapping at end\n\tgo runMyTicker(mClock) // async calls TickerFunc()\n\tcall := trap.Wait(context.Background()) // waits for a call and blocks its return\n\tcall.Release() // allow the TickerFunc() call to return\n\t// optionally check the duration using call.Duration\n\t// Move the clock forward 1 tick\n\tmClock.Advance(time.Second).MustWait(context.Background())\n\t// assert results of the tick\n}\n```\n\nTrapping and then releasing the call to `TickerFunc()` ensures the ticker is started at a\ndeterministic time, so our calls to `Advance()` will have a predictable effect.\n\nTake a look at `TestExampleTickerFunc` in `example_test.go` for a complete worked example.\n\n### Complex time dependence\n\nAnother difficult issue to handle when unit testing is when some code under test makes multiple\ncalls that depend on the time, and you want to simulate some time passing between them.\n\nA very basic example is measuring how long something took:\n\n```go\nvar measurement time.Duration\ngo func(clock quartz.Clock) {\n\tstart := clock.Now()\n\tdoSomething()\n\tmeasurement = clock.Since(start)\n}(mClock)\n\n// how to get measurement to be, say, 5 seconds?\n```\n\nThe two calls into the clock happen asynchronously, so we need to be able to advance the clock after\nthe first call to `Now()` but before the call to `Since()`. Doing this with the libraries we\nmentioned above means that you have to be able to mock out or otherwise block the completion of\n`doSomething()`.\n\nBut, with the trap functionality we mentioned in the previous section, you can deterministically\ncontrol the time each call sees.\n\n```go\ntrap := mClock.Trap().Since()\nvar measurement time.Duration\ngo func(clock quartz.Clock) {\n\tstart := clock.Now()\n\tdoSomething()\n\tmeasurement = clock.Since(start)\n}(mClock)\n\nc := trap.Wait(ctx)\nmClock.Advance(5*time.Second)\nc.Release()\n```\n\nWe wait until we trap the `clock.Since()` call, which implies that `clock.Now()` has completed, then\nadvance the mock clock 5 seconds. Finally, we release the `clock.Since()` call. Any changes to the\nclock that happen _before_ we release the call will be included in the time used for the\n`clock.Since()` call.\n\nAs a more involved example, consider an inactivity timeout: we want something to happen if there is\nno activity recorded for some period, say 10 minutes in the following example:\n\n```go\ntype InactivityTimer struct {\n\tmu sync.Mutex\n\tactivity time.Time\n\tclock quartz.Clock\n}\n\nfunc (i *InactivityTimer) Start() {\n\ti.mu.Lock()\n\tdefer i.mu.Unlock()\n\tnext := i.clock.Until(i.activity.Add(10*time.Minute))\n\tt := i.clock.AfterFunc(next, func() {\n\t\ti.mu.Lock()\n\t\tdefer i.mu.Unlock()\n\t\tnext := i.clock.Until(i.activity.Add(10*time.Minute))\n\t\tif next == 0 {\n\t\t\ti.timeoutLocked()\n\t\t\treturn\n\t\t}\n\t\tt.Reset(next)\n\t})\n}\n```\n\nThe actual contents of `timeoutLocked()` doesn't matter for this example, and assume there are other\nfunctions that record the latest `activity`.\n\nWe found that some time testing libraries hold a lock on the mock clock while calling the function\npassed to `AfterFunc`, resulting in a deadlock if you made clock calls from within.\n\nOthers allow this sort of thing, but don't have the flexibility to test edge cases. There is a\nsubtle bug in our `Start()` function. The timer may pop a little late, and/or some measurable real\ntime may elapse before `Until()` gets called inside the `AfterFunc`. If there hasn't been activity,\n`next` might be negative.\n\nTo test this in Quartz, we'll use a trap. We only want to trap the inner `Until()` call, not the\ninitial one, so to make testing easier we can \"tag\" the call we want. Like this:\n\n```go\nfunc (i *InactivityTimer) Start() {\n\ti.mu.Lock()\n\tdefer i.mu.Unlock()\n\tnext := i.clock.Until(i.activity.Add(10*time.Minute))\n\tt := i.clock.AfterFunc(next, func() {\n\t\ti.mu.Lock()\n\t\tdefer i.mu.Unlock()\n\t\tnext := i.clock.Until(i.activity.Add(10*time.Minute), \"inner\")\n\t\tif next == 0 {\n\t\t\ti.timeoutLocked()\n\t\t\treturn\n\t\t}\n\t\tt.Reset(next)\n\t})\n}\n```\n\nAll Quartz `Clock` functions, and functions on returned timers and tickers support zero or more\nstring tags that allow traps to match on them.\n\n```go\nfunc TestInactivityTimer_Late(t *testing.T) {\n\t// set a timeout on the test itself, so that if Wait functions get blocked, we don't have to\n\t// wait for the default test timeout of 10 minutes.\n\tctx, cancel := context.WithTimeout(10*time.Second)\n\tdefer cancel()\n\tmClock := quartz.NewMock(t)\n\ttrap := mClock.Trap.Until(\"inner\")\n\tdefer trap.Close()\n\n\tit := \u0026InactivityTimer{\n\t\tactivity: mClock.Now(),\n\t\tclock: mClock,\n\t}\n\tit.Start()\n\n\t// Trigger the AfterFunc\n\tw := mClock.Advance(10*time.Minute)\n\tc := trap.Wait(ctx)\n\t// Advance the clock a few ms to simulate a busy system\n\tmClock.Advance(3*time.Millisecond)\n\tc.Release() // Until() returns\n\tw.MustWait(ctx) // Wait for the AfterFunc to wrap up\n\n\t// Assert that the timeoutLocked() function was called\n}\n```\n\nThis test case will fail with our bugged implementation, since the triggered AfterFunc won't call\n`timeoutLocked()` and instead will reset the timer with a negative number. The fix is easy, use\n`next \u003c= 0` as the comparison.\n","funding_links":[],"categories":["Go"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoder%2Fquartz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcoder%2Fquartz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoder%2Fquartz/lists"}