4 min read

Mocking in Go with Tests

Table of Contents

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!

Support my open source work 🚀

Let's Connect

I'm open to discussing new opportunities. Feel free to connect with me or send me an email at [email protected]