skip to content

Doubles and Seams

Substituting a dependency in Go means an interface declared by the consumer and a small struct in _test.go, plus stand-ins for time.Now and the filesystem. Interviewers probe how much you mock.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

17

How do you make a Go type that calls time.Now() testable, and why is a now func() time.Time field the usual seam?

level: juniorimportance: must knowfreq 58%

answer

  1. a package-level function has no seam
  2. in Go a function is a value
  3. the field's type is the signature itself
  4. time.Now already satisfies it
  5. the test assigns a closure over time.Date

basics

~20 s

Store the clock as a func() time.Time field that defaults to time.Now, and read the time through it. A test assigns a closure returning a fixed time.Date value, so results no longer depend on when the test runs.

solid answer

~50 s

time.Now is a package-level function, so there is nothing a test can replace. The fix is to stop calling it directly: give the type a field `now func() time.Time`, read every timestamp through `t.now()`, and default the field to `time.Now` in the constructor. In Go the seam costs almost nothing, because a function is a value and `func() time.Time` is already the whole contract — `time.Now` itself satisfies it, so production wiring is one word and no interface or generated double is needed. A test assigns a closure: `now: func() time.Time { return time.Date(2026, time.March, 8, 23, 59, 0, 0, time.UTC) }`, and assertions on a formatted filename or an expiry decision become exact. Read the clock once per operation and pass the resulting time.Time onward, so two reads inside one unit of work cannot straddle a boundary.

code

go · 11 lines
go
type Reporter struct {
	now func() time.Time
}

func NewReporter() *Reporter {
	return &Reporter{now: time.Now}
}

func (r *Reporter) filename() string {
	return r.now().Format("2006-01-02") + ".log"
}

go deeper

for a junior

Be ready to write the field declaration and the closure a test assigns to it, and to say why time.Now called directly leaves the test nothing to control.

for a middle

Explain why a function value is enough here when other languages need an interface, and show where the production default gets wired so the type still works when nobody supplies a clock.

for a senior

Demonstrate reading the clock once per operation and threading the time.Time onward, and be able to argue against a package-level clock variable on parallel-test grounds.

for a principal

Own whether the clock appears in the exported constructor at all: a required clock parameter is a cost charged to every consumer forever, while an unexported field with a default keeps the seam internal.

## The problem `time.Now` is declared in the standard library as an ordinary package-level function returning a `time.Time`. Go has no way to reassign it, shadow it at run time, or intercept the call. So any function that calls `time.Now()` inside its body has a hidden input that a test cannot supply, and every assertion about the result is really an assertion about the wall clock of whichever machine happens to be running the test. Typical symptoms: a test that asserts a formatted date and passes for 23 hours a day; a test for "the token expires in an hour" that has to sleep; a test that only fails during the first second after midnight, or on the one machine whose clock is a minute fast. ## The seam The Go answer is to make the clock an ordinary dependency of the value that needs it: ```go type Reporter struct { now func() time.Time } func NewReporter() *Reporter { return &Reporter{now: time.Now} } func (r *Reporter) filename() string { return r.now().Format("2006-01-02") + ".log" } ``` Three things are worth noticing. **The field's type is the contract.** `func() time.Time` says everything: no arguments, one instant out. There is no interface to declare, no name to bikeshed, and nothing to keep in sync with an implementation. **`time.Now` already satisfies it.** Functions are first-class values in Go, and a func value's type is its signature, so `now: time.Now` compiles with no adapter. That is why a one-method dependency in Go is usually written as a function value rather than as an interface with one method plus a struct that implements it. **The test supplies a closure.** `func() time.Time { return fixed }` is the whole double. It has no expectations to configure, no ordering to satisfy, and it reads in the test source as data. ## Wiring the default Two defensible styles. Set it in the constructor (`&Reporter{now: time.Now}`), which is explicit and keeps the call site free of checks. Or leave the field nil and fall back at the read site: ```go func (r *Reporter) clock() time.Time { if r.now == nil { return time.Now() } return r.now() } ``` The second keeps the zero value of the struct usable, which matters for a type callers may construct with a plain literal. What you should not do is spread `if r.now == nil` through a dozen methods; funnel it through one place. ## Reading the clock once A subtle bug the seam does not fix by itself: calling `r.now()` several times inside one operation. Even with a real clock those reads return different instants, so a record can be stamped with a start on one side of midnight and an end on the other, and a comparison can decide that something created "now" is already expired. Read once at the top of the operation and pass the `time.Time` value down as a parameter. Time values are small and immutable-by-copy, so passing them is cheap and makes the dependency visible in the signatures of the functions that actually use it. ## What the fake must be able to do A fake that returns one frozen instant forever is perfect for formatting and for "what does this record look like", and useless for anything that waits for time to pass: code that loops until the deadline is reached will never terminate, and any measured duration is zero. When the behaviour under test is about progression, give the fake state: ```go type fakeClock struct{ t time.Time } func (c *fakeClock) Now() time.Time { return c.t } func (c *fakeClock) advance(d time.Duration) { c.t = c.t.Add(d) } ``` and inject `c.Now` as the `func() time.Time`. The method value `c.Now` is itself a `func() time.Time`, so the field type does not change. ## What not to do Do not hide the clock in a package-level variable (`var now = time.Now`) that tests reassign. It compiles, and it is a shared mutable global: two tests running with `t.Parallel()` will fight over it, and the reassignment leaks into every other test in the package until something puts it back. Keep the clock on the value that uses it. Do not sleep in the test to move real time forward. That trades a fast deterministic test for a slow one that is still nondeterministic under load. And do not reach for a mock-generation tool for a dependency whose entire surface is one niladic function. The closure is shorter than the annotation that would generate the mock.

  • Should the default time.Now be wired in the constructor or checked for nil at the read site?
    Either works. Setting it in the constructor is explicit and keeps method bodies clean; a nil check keeps the struct's zero value usable for callers who build it with a plain literal. What matters is that the fallback lives in exactly one place rather than being repeated in every method that needs a timestamp.
  • Why read the injected clock once per operation instead of at every point a timestamp is needed?
    Successive reads return different instants, even from a real clock. A record can then get a start before midnight and an end after it, or an object can look expired at creation. Read once at the entry point and pass the resulting time.Time down as a parameter, which also makes the dependency visible in the inner functions' signatures.
  • What breaks if the fake clock returns the same instant on every call?
    Anything that depends on time moving. A loop that waits for a deadline never finishes, a measured elapsed duration is always zero, and a rate limiter never refills. For those tests give the fake state — a struct holding a time.Time with an advance method — and inject its Now method value, which is still a func() time.Time.
  • Why not put the clock in a package-level variable that tests reassign?
    It is shared mutable global state. Tests marked t.Parallel() overwrite each other's value, a test that forgets to restore it corrupts every later test in the package, and the dependency is invisible in the type's API. Keeping the field on the value that reads the clock scopes the substitution to one instance.

Instead of letting the machine shout the time through the window, the type is handed a small dial it reads. Production sets the dial to follow the wall clock; the test turns it to a chosen minute and leaves it there.

saying these in an interview costs you the question

  • Claims time.Now can be monkey-patched at run time in Go
  • Reaches for a generated double for a one-function dependency
  • Sleeps in the test to reach the next second or day
  • Reads the injected clock several times inside one operation
  • Stores the clock in a package-level var that parallel tests share
  • Says the fake must always return one frozen instant
open as a page

How do you exercise an http.Handler directly in a Go test without binding a port?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Build the request with httptest.NewRequest and pass it, together with an httptest.NewRecorder, straight to the handler's ServeHTTP. The recorder captures status, headers and body in memory, so no port is bound and nothing is served.

open as a page

How do you make a Go service that reads users from a database unit-testable without one?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Declare a small interface in the package that uses the dependency, naming only the one or two methods that code actually calls. Take it as a constructor parameter. In a _test.go file, pass a hand-written struct that implements those methods.

open as a page

Go's `testing` package has no assertion API — how do you check a mock's recorded calls, and when is `t.Fatalf` wrong?

level: middleimportance: must knowfreq 62%

basics

~20 s

You compare with a plain if and report with t.Errorf, using the got/want convention: if got != want { t.Errorf("Update calls = %d, want %d", got, want) }. t.Errorf records the failure and keeps going; t.Fatalf stops the test and is valid only on the goroutine running the test function.

open as a page

What does a `//go:generate` line above an interface do, and when does that command actually run?

level: juniorimportance: should knowfreq 48%

basics

~20 s

It is an ordinary comment that the compiler ignores. Only an explicit go generate run scans source files for lines starting with //go:generate and executes the rest as a command in that package's directory. go build and go test never trigger it.

open as a page

What is testing/fstest.MapFS, and how does a test use it to supply files without touching disk?

level: middleimportance: should knowfreq 34%

basics

~20 s

fstest.MapFS is a map from slash-separated file names to *fstest.MapFile that implements fs.FS. A test declares the whole tree as a map literal and passes it wherever the code takes an fs.FS, so nothing is written to disk.

open as a page

When does a Go test need t.TempDir instead of an in-memory fstest.MapFS?

level: middleimportance: should knowfreq 42%

basics

~20 s

Whenever the code under test needs a real path: it writes or removes files, creates directories, or hands the name to another process. fstest.MapFS is read-only and is not a path at all, so t.TempDir supplies a real directory instead.

open as a page

What does httptest.ResponseRecorder.Result() return, and when is it safe to call?

level: middleimportance: should knowfreq 58%

basics

~20 s

Result builds an *http.Response from what the handler recorded: status, a header snapshot taken at the first write, and a readable body. Call it only after the handler has finished running, then assert on that response.

open as a page

A Go test's hand-written fake must implement six methods though the code calls two. What went wrong?

level: middleimportance: should knowfreq 55%

basics

~20 s

The interface was taken from the implementation's whole surface instead of being declared by the code that uses it. Redeclare it in the consuming package with just the two methods called; the real type still satisfies it, and the fake shrinks to two methods.

open as a page

An httptest.NewServer handler sleeps 2s, the SDK sets http.Client.Timeout to 1s and the caller passes a 500ms context — which deadline surfaces, and how do you prove it?

level: seniorimportance: should knowfreq 42%

basics

~10 s

The shortest one wins: the caller's 500ms context deadline ends the request first. Prove it by checking ctx.Err() is context.DeadlineExceeded, since a Client.Timeout error also reports a timeout and leaves the context unexpired.

open as a page

Your generated mock pins the exact sequence of calls a reconciler makes, and a behaviour-preserving refactor turns the suite red. What do you change?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Read the unmet-expectation report to confirm the outcome is unchanged and only the call path moved, then rewrite the expectations to assert outcomes: allow reads any number of times and in any order, pin only the writes the contract promises, and match on the argument fields that matter rather than whole structs.

open as a page

A Go handler's tests all pass with a hand-written fake, yet production fails on lookup errors. Why?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The fake's method returns the zero value and a nil error for every call, so the code's error branch never executes in any test. Give the fake per-case return values, add cases where it fails, and make an unconfigured call fail the test loudly.

open as a page

Your team must decide whether to generate mocks from interfaces or hand-write doubles. How do you make that call?

level: principalimportance: should knowfreq 33%

basics

~20 s

Decide on scale and churn, not taste. Generation pays when many wide interfaces change often; it costs a pinned tool dependency every contributor and CI must have, plus a CI check that regenerated output matches what is committed. Few narrow interfaces do not repay that.

open as a page

You own a Go package many teams import. Should you export an interface for their fakes, or a concrete type?

level: principalimportance: should knowfreq 38%

basics

~20 s

Default to returning the concrete type and letting each consumer declare the one or two methods it needs. An exported interface that downstream teams implement in tests is frozen API: every method you add breaks their stand-ins, and you cannot withdraw it.

open as a page

Why does http.Get fail against an httptest.NewTLSServer URL, and what fixes it?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

The test server serves an https URL with its own certificate, which no system trust store knows, so the default client rejects it. Use the client the server hands you: srv.Client() already trusts that certificate.

open as a page

In Go, what do you trade away when a test fake embeds the interface it is meant to satisfy?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

You trade a compile error for a runtime panic. The embedded interface field is nil but supplies every method, so the fake always compiles; calling a method you did not override dereferences that nil interface and panics instead of failing to build.

open as a page

Why does injecting a fake clock not fix a Go test for a local-midnight log rollover that fails only on CI?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Because the instant is only half the input. Turning a time.Time into a calendar day uses its location, and code that leans on time.Local gets a different day on a machine set to UTC than on a laptop. Inject the *time.Location as well.

open as a page