Mocking in Go with Interfaces and Fakes

How to test Go code that calls databases, APIs or the clock by depending on small interfaces and writing simple fakes, without a mocking framework.

"How do I mock in Go?" usually means: my function talks to a database, an email service or the current time, and I want to test it without the real thing. In Go, the usual answer does not need a library. You make the code depend on a small interface, and in tests you pass a hand-written fake that satisfies it.

The Problem: a Hard-Wired Dependency

func Register(email string) error {
	db := sql.Open(...)                // real database
	...
	smtp.SendMail(...)                 // real email
}

Testing this sends real emails and needs a real database. The function creates its dependencies itself, so a test cannot swap them out.

Step 1: Depend on Small Interfaces

Pass the dependencies in, typed as interfaces with only the methods this code uses:

// file: signup_test.go
package signup

import (
	"errors"
	"fmt"
	"strings"
	"testing"
)

// The code under test.

type UserStore interface {
	Exists(email string) (bool, error)
	Save(email string) error
}

type Mailer interface {
	Send(to, subject string) error
}

var ErrTaken = errors.New("email already registered")

type Service struct {
	Store  UserStore
	Mailer Mailer
}

func (s *Service) Register(email string) error {
	email = strings.ToLower(strings.TrimSpace(email))
	taken, err := s.Store.Exists(email)
	if err != nil {
		return fmt.Errorf("check email: %w", err)
	}
	if taken {
		return ErrTaken
	}
	if err := s.Store.Save(email); err != nil {
		return fmt.Errorf("save user: %w", err)
	}
	return s.Mailer.Send(email, "Welcome")
}

// Fakes used by the tests.

type fakeStore struct {
	users   map[string]bool
	saveErr error
}

func (f *fakeStore) Exists(email string) (bool, error) { return f.users[email], nil }

func (f *fakeStore) Save(email string) error {
	if f.saveErr != nil {
		return f.saveErr
	}
	f.users[email] = true
	return nil
}

type fakeMailer struct{ sent []string }

func (f *fakeMailer) Send(to, subject string) error {
	f.sent = append(f.sent, to+": "+subject)
	return nil
}

// Tests.

func TestRegisterSendsWelcome(t *testing.T) {
	store := &fakeStore{users: map[string]bool{}}
	mail := &fakeMailer{}
	svc := &Service{Store: store, Mailer: mail}

	if err := svc.Register("  Asha@Example.com "); err != nil {
		t.Fatalf("Register: %v", err)
	}
	if !store.users["asha@example.com"] {
		t.Error("user was not saved in lowercase")
	}
	if len(mail.sent) != 1 || mail.sent[0] != "asha@example.com: Welcome" {
		t.Errorf("sent = %v", mail.sent)
	}
}

func TestRegisterTaken(t *testing.T) {
	store := &fakeStore{users: map[string]bool{"asha@example.com": true}}
	mail := &fakeMailer{}
	svc := &Service{Store: store, Mailer: mail}

	err := svc.Register("asha@example.com")
	if !errors.Is(err, ErrTaken) {
		t.Fatalf("err = %v, want ErrTaken", err)
	}
	if len(mail.sent) != 0 {
		t.Error("no email should be sent for a taken address")
	}
}

func TestRegisterSaveFails(t *testing.T) {
	store := &fakeStore{users: map[string]bool{}, saveErr: errors.New("disk full")}
	svc := &Service{Store: store, Mailer: &fakeMailer{}}

	if err := svc.Register("a@b.in"); err == nil {
		t.Fatal("expected an error when saving fails")
	}
}

Everything is in one file so it runs as is. In a real project, Service and the interfaces live in signup.go, and only the fakes and tests are in signup_test.go.

In production, Service gets a real Postgres-backed store and an SMTP mailer. In tests, it gets fakes made of a map and a slice. The fakes let tests check what happened: was the user saved, was exactly one email sent. They also let you trigger failures, like saveErr, that are hard to cause with real services.

Fakes vs Mocks

  • A fake is a working, simplified implementation, like a map instead of a database.
  • A mock records calls and checks they happened as expected, often generated by a tool.

Go developers lean towards fakes, because they are plain Go code, easy to read, and do not break when you change the order of calls. Mock generators like mockery or gomock exist and are useful for large interfaces, but most Go code does not need them.

Big interfaces make faking painful:

If UserStore had twenty methods, every fake would need twenty methods. Define interfaces where they are used, with only the methods that code calls. A two-method interface is easy to fake by hand.

Faking the Clock

Code that calls time.Now() directly is hard to test. Pass in a function instead:

// file: expiry_test.go
package auth

import (
	"testing"
	"time"
)

type Token struct {
	Expires time.Time
}

func (t Token) Valid(now func() time.Time) bool {
	return now().Before(t.Expires)
}

func TestTokenValid(t *testing.T) {
	exp := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
	tok := Token{Expires: exp}

	before := func() time.Time { return exp.Add(-time.Minute) }
	after := func() time.Time { return exp.Add(time.Minute) }

	if !tok.Valid(before) {
		t.Error("token should be valid one minute before expiry")
	}
	if tok.Valid(after) {
		t.Error("token should be invalid one minute after expiry")
	}
}

In production you call tok.Valid(time.Now). Tests control time exactly, with no sleeping.

Function Fields for One-Off Behavior

For a single test that needs unusual behavior, a fake with function fields avoids writing a new type:

type funcMailer struct {
	send func(to, subject string) error
}

func (f funcMailer) Send(to, subject string) error { return f.send(to, subject) }

// in a test:
m := funcMailer{send: func(to, s string) error { return errors.New("smtp down") }}

Global variables swapped in tests:

Replacing a package-level variable like var now = time.Now in tests works, but it breaks as soon as tests run in parallel, and the race detector reports it. Passing dependencies in through struct fields or parameters avoids that.

Testable design:

If every external dependency of your business logic arrives through a small interface or a function parameter, tests can use fakes, run in milliseconds, and cover failure paths that are hard to reproduce for real.

Summary

Make code testable by passing dependencies in as small interfaces defined where they are used, then write simple fakes for tests: a map for a store, a slice for a mailer, a function for the clock. Fakes let you check results and trigger failures without real services. Mock generators exist, but plain fakes cover most needs and stay readable.