Search and navigate

Pages

Start HereRoadmapTutorialsCheatsheetInterview QuestionsResources

Published tutorials

Building a REST API with Go: The Complete TutorialGeneric Functions in Go: Writing Reusable CodeGo Benchmarking: Measuring Performance with testing.BGo Channels: Communication Between GoroutinesGo Constraints: comparable, Ordered and Custom SetsGo Context: Cancellation, Deadlines, and Request ValuesGo HTTP Client: Timeouts, JSON, and Reliable RequestsGo HTTP Middleware: Build a Production ChainGo Interview Questions: 25 Answers That Show UnderstandingGo Mutex: Protecting Shared State from Race ConditionsGo net/http: Build an HTTP Server from ScratchGo select Statement: Multiplexing Channels ExplainedGo Tools You Need for a Reliable Development LoopGo Tutorial: From First Program to Backend FoundationsGo Type Parameters: Syntax and Constraints ExplainedGo Worker Pool Pattern: Bounded Concurrency at ScaleGo's Type System Deep Dive: Static Typing Done RightGolang Compilation and Execution ExplainedGolang File Structure for Modules and ApplicationsGolang Hello World and Your First Go ProgramGolang IDEs: Choose an Editor for Your WorkflowGolang Interfaces: What They Are and How to Use ThemGoroutine Leaks in Go: How to Detect and Prevent ThemGoroutines in Go: Concurrency Without LeaksHow to Download and Install Golang SafelyHow to Learn Golang with a Focused Study PlanJSON in Go: Encoding, Decoding, and ValidationLearn Golang with a Practical Beginner CourseMocking in Go: Test Doubles Without Magicsync.WaitGroup in Go: Coordinating Multiple GoroutinesTable-Driven Tests in Go: The Idiomatic WayTesting in Go: Table Tests, HTTP, Fakes, and CoverageWhat Is Go and How Does a Go Program Work?Why Learn Golang and What Is Go Used For?Writing Unit Tests in Go: A Practical Guide
Open full search and filters →

Mocking in Go: Test Doubles Without Magic

Learn golang mocking the idiomatic way: small interfaces plus hand-written fakes, spies, and httptest, with generated mocks only when they earn it.

Standard-library lessonGo requirement: Go 1.21+ (stdlib examples tested on Go 1.24.7)How tutorials are checked
Testing · Lesson 5Saved in this browser. No account required.
A component connected through a small interface to interchangeable real and fake dependencies
Small interfaces make dependencies replaceable without a heavy mocking framework. Image: Golang Tutorial

Mocking in Go: Test Doubles Without Magic

This tutorial shows how to test Go code that depends on databases, payment gateways, clocks, and HTTP services without a mocking framework. You will define small interfaces, hand-write fakes and spies, inject time and randomness, and use httptest. Then you will see exactly when a generated mock earns its keep.

Works with Go 1.21+. Every stdlib example here was compiled and run on Go 1.24.7, and the output shown is real. Framework snippets that need a third-party library are labeled as not executed. If Go is new to you, read the complete Go tutorial first, and for the deeper testing picture see the Go testing guide.

The core idea: depend on a small interface, substitute a test double

Most “how do I mock this in Go” questions have the same answer, and it is not a library. Your code under test should depend on a small interface, not a concrete type. In the test, you pass a different implementation of that interface. That substitute is a test double. That is the entire mechanism.

Take time. Code that calls time.Now() directly is untestable: you cannot assert on a value that changes every nanosecond. So you do not call time.Now(). You depend on the smallest interface that describes what you need:

type Clock interface {
	Now() time.Time
}

Production passes a real clock. The test passes one that returns a fixed instant:

type fixedClock struct {
	t time.Time
}

func (c fixedClock) Now() time.Time { return c.t }

No framework, no code generation, no magic. The double is nine lines you can read. This works because Go satisfies interfaces implicitly: fixedClock never declares that it implements Clock, it just has a Now() method, so it qualifies. The rest of this article is that same move applied to gateways, repositories, randomness, and HTTP.

The four kinds of test double, defined precisely

People say “mock” for all of these, which is why the topic is confusing. The categories come from Gerard Meszaros, and the distinctions are worth keeping straight because they change what your test asserts on.

  • Stub: returns canned answers, records nothing. fixedClock above is a stub. You use it to feed a specific input into the code under test.
  • Fake: a real, working implementation that takes a shortcut unsuitable for production. An in-memory map standing in for a database is a fake. It has behavior: store something, get it back.
  • Spy: a stub that also records how it was called, so the test can inspect the calls afterward. “Was Charge called once, with 1500 cents?”
  • Mock: a double pre-programmed with expectations that it verifies itself. You tell it up front “expect Charge exactly once with these arguments,” and it fails the test if reality differs. This is what mocking frameworks generate.

The practical takeaway: stubs and fakes control inputs, spies and mocks assert on interactions. The first three you hand-write in a few lines. Only the last one, the self-verifying mock with expectations, is what libraries like gomock and testify exist to produce, and you need it far less often than the SERP results imply.

Hand-writing a fake that implements your interface

A fake carries real behavior. Suppose checkout depends on a payment gateway:

type Gateway interface {
	Charge(customerID string, cents int) (txID string, err error)
	Refund(txID string) error
}

The real implementation calls Stripe. The fake keeps transactions in a map, enforces the same rules (you cannot refund a transaction twice, you cannot charge a non-positive amount), and records charges for assertions:

type FakeGateway struct {
	nextID   int
	captured map[string]int // txID -> cents charged
	Charges  []string       // customerIDs, in call order
}

func NewFakeGateway() *FakeGateway {
	return &FakeGateway{captured: make(map[string]int)}
}

func (f *FakeGateway) Charge(customerID string, cents int) (string, error) {
	if cents <= 0 {
		return "", errors.New("charge amount must be positive")
	}
	f.nextID++
	txID := fmt.Sprintf("tx_%d", f.nextID)
	f.captured[txID] = cents
	f.Charges = append(f.Charges, customerID)
	return txID, nil
}

func (f *FakeGateway) Refund(txID string) error {
	if _, ok := f.captured[txID]; !ok {
		return fmt.Errorf("unknown transaction %q", txID)
	}
	delete(f.captured, txID)
	return nil
}

// Compile-time proof the fake satisfies the interface.
var _ Gateway = (*FakeGateway)(nil)

That var _ Gateway = (*FakeGateway)(nil) line is worth copying into your own fakes. It makes the compiler verify the fake still satisfies the interface, so if you add a method to Gateway later, the build breaks at the fake instead of silently at a call site.

The test drives the fake through a real charge-then-refund flow and confirms the fake behaves like the contract:

func TestFakeGateway_ChargeThenRefund(t *testing.T) {
	gw := NewFakeGateway()

	txID, err := gw.Charge("cus_7", 2000)
	if err != nil {
		t.Fatalf("Charge() error: %v", err)
	}
	if len(gw.Charges) != 1 || gw.Charges[0] != "cus_7" {
		t.Errorf("Charges = %v, want [cus_7]", gw.Charges)
	}

	if err := gw.Refund(txID); err != nil {
		t.Fatalf("Refund(%s) error: %v", txID, err)
	}
	// Refunding the same transaction twice must fail: the fake enforces it.
	if err := gw.Refund(txID); err == nil {
		t.Error("second Refund() should have failed, got nil")
	}
}
=== RUN   TestFakeGateway_ChargeThenRefund
--- PASS: TestFakeGateway_ChargeThenRefund (0.00s)
=== RUN   TestFakeGateway_RejectsBadAmount
--- PASS: TestFakeGateway_RejectsBadAmount (0.00s)
PASS
ok  	pay	0.002s

A good fake is reusable across your whole test suite. Write it once next to the interface and every test that needs a gateway gets a fast, deterministic one.

A spy records calls so you can assert on them

Sometimes you do not want working behavior, you want to know how a collaborator was called. That is a spy: a minimal double that appends each call to a slice.

type spyGateway struct {
	charges []chargeCall
	err     error
}

type chargeCall struct {
	customerID string
	cents      int
}

func (g *spyGateway) Charge(customerID string, cents int) (string, error) {
	g.charges = append(g.charges, chargeCall{customerID, cents})
	if g.err != nil {
		return "", g.err
	}
	return "tx_123", nil
}

The err field lets the same type double as a stub: set it and every Charge returns that error, which is how you test the failure path without a real declined card. After exercising the code, you assert on the recorded slice:

if len(gateway.charges) != 1 {
	t.Fatalf("got %d charges, want 1", len(gateway.charges))
}
got := gateway.charges[0]
if got.customerID != "cus_9" || got.cents != 1500 {
	t.Errorf("charge = %+v, want {cus_9 1500}", got)
}

Recording the arguments as a slice of structs, rather than tracking a boolean wasCalled, is the pattern that scales. You get call count and every argument for free, and the failure message prints the actual calls.

Faking time and randomness by injecting them

Time and randomness are the two dependencies people forget are dependencies. Both are non-deterministic global state, and both become testable the moment you inject them.

Randomness follows the same rule as time: take it as a parameter. A password reset code generator should not reach for a package-level random source. It should accept one:

// IntSource is anything that can produce a non-negative int below n.
// *math/rand.Rand satisfies it, so production code passes a real one.
type IntSource interface {
	Intn(n int) int
}

func GenerateCode(src IntSource) string {
	const digits = 6
	code := make([]byte, digits)
	for i := range code {
		code[i] = byte('0' + src.Intn(10))
	}
	return string(code)
}

In production you pass a real *rand.Rand. In the test you pass one seeded with a constant, which makes the sequence reproducible, so you can assert on the exact output:

func TestGenerateCode_IsDeterministicWithSeededSource(t *testing.T) {
	src := rand.New(rand.NewSource(1)) // fixed seed -> reproducible
	got := GenerateCode(src)
	const want = "177918"
	if got != want {
		t.Errorf("GenerateCode() = %q, want %q", got, want)
	}
}
=== RUN   TestGenerateCode_IsDeterministicWithSeededSource
--- PASS: TestGenerateCode_IsDeterministicWithSeededSource (0.00s)
PASS
ok  	resetcode	0.004s

The seed 1 produces 177918 every run on every machine. That is the payoff of injection: the “random” code is fixed in the test but genuinely random in production. Note you did not mock math/rand. You depended on a one-method interface it happens to satisfy, which is the idiomatic move and avoids the “do not mock what you do not own” trap covered below.

Faking an HTTP dependency with httptest.Server, not a mock client

When your code calls an external API, the instinct is to mock the HTTP client. Do not. Go ships net/http/httptest, which starts a real HTTP server on a loopback port. You point your client at it and control exactly what it returns. You test the real request path, real serialization, real status-code handling, with no network.

The key design choice is making the base URL injectable so the test can redirect it:

type WeatherClient struct {
	baseURL string
	http    *http.Client
}

func NewWeatherClient(baseURL string) *WeatherClient {
	return &WeatherClient{
		baseURL: baseURL,
		http:    &http.Client{Timeout: 5 * time.Second},
	}
}

func (c *WeatherClient) CurrentTempC(city string) (float64, error) {
	url := fmt.Sprintf("%s/v1/current?city=%s", c.baseURL, city)
	resp, err := c.http.Get(url)
	if err != nil {
		return 0, fmt.Errorf("weather request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return 0, fmt.Errorf("weather API returned %s", resp.Status)
	}
	var f forecast
	if err := json.NewDecoder(resp.Body).Decode(&f); err != nil {
		return 0, fmt.Errorf("decode forecast: %w", err)
	}
	return f.TempC, nil
}

The test stands up a server, hands its URL to the client, and asserts on the parsed result. It even inspects the incoming query string, so the server doubles as a spy on the request:

func TestCurrentTempC_ParsesResponse(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got := r.URL.Query().Get("city"); got != "Berlin" {
			t.Errorf("city query = %q, want Berlin", got)
		}
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, `{"city":"Berlin","temp_c":19.5}`)
	}))
	defer server.Close()

	client := NewWeatherClient(server.URL)
	temp, err := client.CurrentTempC("Berlin")
	if err != nil {
		t.Fatalf("CurrentTempC() error: %v", err)
	}
	if temp != 19.5 {
		t.Errorf("temp = %v, want 19.5", temp)
	}
}
=== RUN   TestCurrentTempC_ParsesResponse
--- PASS: TestCurrentTempC_ParsesResponse (0.00s)
=== RUN   TestCurrentTempC_ServerError
--- PASS: TestCurrentTempC_ServerError (0.00s)
PASS
ok  	resetcode	0.007s

For the full picture of building and testing clients this way, see the Go HTTP client guide. httptest.Server is almost always a better choice than a mocked http.Client, because it exercises code a mock would skip.

Layer 3: unit-testing a service that depends on a repository and a clock

Now the real shape of production code. A billing service depends on three collaborators: a repository, a payment gateway, and a clock. Each is a small interface defined next to the service that uses it:

type Clock interface {
	Now() time.Time
}

type PaymentGateway interface {
	Charge(customerID string, cents int) (txID string, err error)
}

type SubscriptionRepo interface {
	Find(id string) (Subscription, error)
	Save(sub Subscription) error
}

func (s *Service) Renew(subID string) error {
	sub, err := s.repo.Find(subID)
	if err != nil {
		return fmt.Errorf("renew %s: %w", subID, err)
	}
	if !sub.Active {
		return fmt.Errorf("renew %s: subscription is inactive", subID)
	}
	if _, err := s.gateway.Charge(sub.CustomerID, sub.PriceCents); err != nil {
		return fmt.Errorf("renew %s: charge failed: %w", subID, err)
	}
	sub.LastRenewed = s.clock.Now()
	if err := s.repo.Save(sub); err != nil {
		return fmt.Errorf("renew %s: save failed: %w", subID, err)
	}
	return nil
}

The test wires up three hand-written doubles, one of each kind: fixedClock (stub), inMemoryRepo (fake, a map-backed repository), and spyGateway (spy). One test checks the success path end to end, asserting on both the spy and the fake. A second checks that a declined charge never writes the renewal timestamp:

func TestRenew_ChargesAndStampsTime(t *testing.T) {
	repo := newInMemoryRepo()
	repo.Save(Subscription{ID: "sub_1", CustomerID: "cus_9", PriceCents: 1500, Active: true})

	gateway := &spyGateway{}
	renewedAt := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
	svc := NewService(repo, gateway, fixedClock{t: renewedAt})

	if err := svc.Renew("sub_1"); err != nil {
		t.Fatalf("Renew() returned error: %v", err)
	}
	if len(gateway.charges) != 1 {
		t.Fatalf("got %d charges, want 1", len(gateway.charges))
	}
	saved, _ := repo.Find("sub_1")
	if !saved.LastRenewed.Equal(renewedAt) {
		t.Errorf("LastRenewed = %v, want %v", saved.LastRenewed, renewedAt)
	}
}

func TestRenew_ChargeFailure_IsNotSaved(t *testing.T) {
	repo := newInMemoryRepo()
	repo.Save(Subscription{ID: "sub_1", CustomerID: "cus_9", PriceCents: 1500, Active: true})

	declined := errors.New("card declined")
	gateway := &spyGateway{err: declined}
	svc := NewService(repo, gateway, fixedClock{t: time.Now()})

	if err := svc.Renew("sub_1"); !errors.Is(err, declined) {
		t.Fatalf("Renew() error = %v, want it to wrap %v", err, declined)
	}
	saved, _ := repo.Find("sub_1")
	if !saved.LastRenewed.IsZero() {
		t.Errorf("LastRenewed = %v, want zero (charge failed)", saved.LastRenewed)
	}
}
=== RUN   TestRenew_ChargesAndStampsTime
--- PASS: TestRenew_ChargesAndStampsTime (0.00s)
=== RUN   TestRenew_ChargeFailure_IsNotSaved
--- PASS: TestRenew_ChargeFailure_IsNotSaved (0.00s)
PASS
ok  	billing	0.002s

Note errors.Is in the failure test: because Renew wraps errors with %w, the test asserts the declined error propagates without matching on the exact string. That pattern is worth its own read in the error handling guide. This is a complete unit test of business logic with zero infrastructure, running in two milliseconds, using doubles you can read top to bottom.

When a generated mock or testify/mock earns its place

Hand-written fakes cover the large majority of cases. Two situations justify reaching for a framework.

Large interfaces. If an interface has fifteen methods and a test only cares about two, hand-writing a fake means stubbing thirteen methods that just panic. A generator does that for you. Strict call-order or exact-argument assertions. When the test genuinely needs “method A must be called before method B, exactly once, with these arguments, and nothing else,” a self-verifying mock expresses that directly.

The maintained generator is go.uber.org/mock (v0.5.2, the actively maintained successor to the now-archived github.com/golang/mock). You generate a mock from an interface with mockgen:

# not executed here
go run go.uber.org/mock/mockgen@latest -source=billing.go -destination=mock_gateway_test.go -package=billing

Then drive it with expectations. The following is idiomatic gomock, verified against the current docs, not executed in this sandbox because the module cannot be fetched here:

// gomock (go.uber.org/mock v0.5.2). Not executed.
func TestRenew_WithGomock(t *testing.T) {
	ctrl := gomock.NewController(t)
	gateway := NewMockPaymentGateway(ctrl)

	gateway.EXPECT().
		Charge("cus_9", 1500).
		Return("tx_123", nil).
		Times(1)

	// ... wire gateway into the service and call Renew ...
}

ctrl verifies every expectation when the test ends. stretchr/testify (v1.10.0) takes a different shape: you embed mock.Mock in a hand-written type and program it inline, which reads well when you want assertions and doubles in one toolkit:

// testify/mock (v1.10.0). Not executed.
type MockGateway struct {
	mock.Mock
}

func (m *MockGateway) Charge(customerID string, cents int) (string, error) {
	args := m.Called(customerID, cents)
	return args.String(0), args.Error(1)
}

func TestRenew_WithTestify(t *testing.T) {
	gateway := new(MockGateway)
	gateway.On("Charge", "cus_9", 1500).Return("tx_123", nil).Once()

	// ... call Renew ...

	gateway.AssertExpectations(t)
}

Both are correct and widely used. Reach for them when the interface is large or the ordering assertions are strict. For the billing service above, the hand-written doubles are shorter, faster to read, and do not add a dependency, so they win.

Common mistakes

Mocking types you do not own. Do not build a double for *sql.DB, *redis.Client, or a vendor SDK. You do not control their contracts, your double will drift from real behavior, and the test passes while production breaks. Wrap the dependency behind a small interface of your own and fake that, or use a real instance in an integration test.

Over-specifying call order. A test that asserts “Charge then Save then Log, in exactly that sequence” fails the moment you reorder two harmless lines, even though behavior is unchanged. Assert on outcomes (the charge happened, the row was saved) rather than the precise choreography, unless order is genuinely part of the contract.

Mocking the standard library. You saw the fix already: inject a Clock or an IntSource interface instead of trying to intercept time.Now() or rand.Intn. The stdlib is a dependency you wrap, not one you mock.

Producer-side interfaces defined only to enable mocking. A common anti-pattern is the package that provides PostgresRepo also exporting a Repository interface listing all its methods, purely so consumers can mock it. Define the interface where it is used, listing only the methods that caller needs. The interfaces guide covers why consumer-side interfaces stay small and stable while producer-side ones bloat.

Fakes that drift from real behavior. A fake is only useful if it obeys the same contract as production. If the real gateway rejects zero-amount charges, your fake must too, or your tests certify behavior that does not exist. When you find such a gap, add a shared contract test that runs against both the fake and, in an integration suite, the real implementation.

What next

You now have the idiomatic Go approach to test doubles: small consumer-side interfaces, hand-written stubs, fakes, and spies, injected time and randomness, and httptest for HTTP dependencies, with generated mocks held in reserve for large interfaces and strict ordering.

  • Ground this in the full Go testing guide, the pillar for the testing package, coverage, and test layout.
  • Combine doubles with table-driven tests to cover many cases with one test function.
  • Revisit Go interfaces to sharpen the consumer-side design that makes all of this possible.
  • If you are still finding your footing, the complete Go tutorial covers the structs and methods these examples assume.