Hey there! 👋 Today we’re diving into mocking in Go — a super useful technique that’ll make your tests faster and more reliable.
No more waiting 3 seconds for a countdown test to finish!
Let’s break it down step by step. 🚀
The Problem: A Slow Countdown
You’ve been asked to write a countdown:
3
2
1
Go!
Simple, right?
But there’s a catch—each number should print after a 1-second delay.
First Attempt: The Naive Way
Here’s the initial code:
func Countdown(out io.Writer) {
for i := 3; i > 0; i-- {
fmt.Fprintln(out, i)
time.Sleep(1 * time.Second) // 😬 Problem: Slow tests!
}
fmt.Fprint(out, "Go!")
}
The Issue?
- Tests take 3+ seconds to run.
- Slow feedback = unhappy devs.
- What if we need more tests? Do we really want minutes of waiting?
The Solution: Mocking time.Sleep
We need to fake the sleep in tests but keep the real sleep in production.
Step 1: Define a Sleeper
Interface
type Sleeper interface {
Sleep()
}
Now, our Countdown
can accept any sleeper (real or fake).
Step 2: Create a Spy Sleeper
A spy records how it’s used.
type SpySleeper struct {
Calls int
}
func (s *SpySleeper) Sleep() {
s.Calls++ // Just count calls, no real sleep!
}
Step 3: Update Countdown
func Countdown(out io.Writer, sleeper Sleeper) {
for i := 3; i > 0; i-- {
fmt.Fprintln(out, i)
sleeper.Sleep() // Uses the injected sleeper
}
fmt.Fprint(out, "Go!")
}
Now, tests run instantly because we replace time.Sleep
with our spy.
Testing the Order of Operations
But wait—what if Sleep()
happens after printing instead of before? Our test wouldn’t catch that!
Introducing SpyCountdownOperations
This spy records both writes and sleeps to check the correct order:
type SpyCountdownOperations struct {
Calls []string
}
func (s *SpyCountdownOperations) Sleep() {
s.Calls = append(s.Calls, "sleep")
}
func (s *SpyCountdownOperations) Write(p []byte) (n int, err error) {
s.Calls = append(s.Calls, "write")
return len(p), nil
}
Now, we can verify the exact sequence:
want := []string{
"write", "sleep", // 3
"write", "sleep", // 2
"write", "sleep", // 1
"write", // "Go!"
}
Making Sleep Configurable
What if we want a custom sleep duration?
Introducing ConfigurableSleeper
type ConfigurableSleeper struct {
Duration time.Duration
SleepFn func(time.Duration) // Allows mocking time.Sleep
}
func (c *ConfigurableSleeper) Sleep() {
c.SleepFn(c.Duration) // Calls the injected sleep function
}
Now, in production:
sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep}
Countdown(os.Stdout, sleeper)
And in tests:
spyTime := &SpyTime{}
sleeper := ConfigurableSleeper{5 * time.Second, spyTime.Sleep}
sleeper.Sleep()
if spyTime.durationSlept != 5 * time.Second {
t.Error("Sleep duration was wrong!")
}
But Isn’t Mocking Evil?
Good question! Mocking gets a bad rap when:
- You mock too much (3+ mocks? Red flag! 🚩).
- Tests become brittle (break on tiny refactors).
- You test implementation details instead of behavior.
Golden Rule: ✅ Test what your code does, not how it does it.
Key Takeaways
- Mock slow dependencies (like
time.Sleep
) for faster tests. - Use interfaces to swap real and fake implementations.
- Keep tests simple — focus on behavior, not internal details.
Now go forth and mock responsibly! 🎉
Next Up: Concurrency in Go!