Mocking in a Language Without Mockito

In C#, you reach for Moq or NSubstitute without thinking. Interface? Mock it. Verify calls? Easy. Set up return values? One line.

Go doesn’t have a standard mocking framework. There’s no reflection-based proxy generation. No Mock<IUserService>(). The Go way is more manual, and that’s… actually fine? Let me explain.

The C# Approach

You’ve done this a thousand times:

public interface IUserRepository
{
    User GetById(int id);
    void Save(User user);
}

[Fact]
public void ProcessUser_CallsRepository()
{
    var mockRepo = new Mock<IUserRepository>();
    mockRepo.Setup(r => r.GetById(1)).Returns(new User { Id = 1, Name = "Alice" });
    
    var service = new UserService(mockRepo.Object);
    service.Process(1);
    
    mockRepo.Verify(r => r.GetById(1), Times.Once);
}

Clean, expressive, powerful. Moq generates a proxy class at runtime that implements the interface and records calls.

The Go Approach: Hand-Written Fakes

Go doesn’t have reflection-based mocking (well, it does, but it’s not idiomatic). Instead, you write your own test doubles:

type UserRepository interface {
    GetByID(id int) (*User, error)
    Save(user *User) error
}

// Fake implementation for testing
type FakeUserRepo struct {
    Users map[int]*User
    SaveCalled bool
    SavedUser *User
}

func (f *FakeUserRepo) GetByID(id int) (*User, error) {
    user, ok := f.Users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return user, nil
}

func (f *FakeUserRepo) Save(user *User) error {
    f.SaveCalled = true
    f.SavedUser = user
    return nil
}

Then in your test:

func TestProcessUser(t *testing.T) {
    repo := &FakeUserRepo{
        Users: map[int]*User{
            1: {ID: 1, Name: "Alice"},
        },
    }
    
    service := NewUserService(repo)
    service.Process(1)
    
    if !repo.SaveCalled {
        t.Error("expected Save to be called")
    }
}

More code. More manual work. But you control everything, and there’s no magic.

Why Go Developers Accept This

Three reasons:

1. Interfaces are small

Go’s implicit interfaces encourage tiny interfaces. One or two methods. Writing a fake for a two-method interface is trivial.

type Saver interface {
    Save(user *User) error
}

// Fake is one function
type FakeSaver struct {
    Err error
}

func (f *FakeSaver) Save(user *User) error {
    return f.Err
}

Compare to C#’s larger interfaces with 10+ methods. Moq saves significant effort there.

2. Fakes are reusable

You write the fake once, use it everywhere. Put it in a testing package or _test.go file:

// In repository/testing.go or repository/fake_test.go
type FakeUserRepo struct { ... }

Now all tests can use it. The upfront cost pays off.

3. Explicit is better than magic

Go’s philosophy. When a test fails, you can see exactly what the fake does. No proxy magic. No setup/verify DSL to decode.

The testify/mock Package

If you really want Moq-style mocking, testify provides it:

import "github.com/stretchr/testify/mock"

type MockUserRepo struct {
    mock.Mock
}

func (m *MockUserRepo) GetByID(id int) (*User, error) {
    args := m.Called(id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

func (m *MockUserRepo) Save(user *User) error {
    args := m.Called(user)
    return args.Error(0)
}

Then use it:

func TestProcessUser(t *testing.T) {
    repo := new(MockUserRepo)
    repo.On("GetByID", 1).Return(&User{ID: 1, Name: "Alice"}, nil)
    repo.On("Save", mock.Anything).Return(nil)
    
    service := NewUserService(repo)
    service.Process(1)
    
    repo.AssertCalled(t, "Save", mock.Anything)
}

Closer to Moq, but you still write the mock struct manually. testify handles the setup/verification plumbing.

Generated Mocks: mockgen and mockery

Tools can generate mock implementations:

mockgen (from gomock):

mockgen -source=repository.go -destination=mock_repository.go

mockery:

mockery --name=UserRepository

These generate the boilerplate. You still use them manually in tests.

Patterns for Effective Fakes

Function Fields

For simple cases, use function fields:

type FakeNotifier struct {
    NotifyFunc func(userID int, message string) error
}

func (f *FakeNotifier) Notify(userID int, message string) error {
    if f.NotifyFunc != nil {
        return f.NotifyFunc(userID, message)
    }
    return nil
}

In tests:

notifier := &FakeNotifier{
    NotifyFunc: func(userID int, message string) error {
        if userID != 1 {
            t.Errorf("unexpected userID: %d", userID)
        }
        return nil
    },
}

Inline behaviour per test. No shared state.

Recording Calls

Track what was called:

type FakeNotifier struct {
    Calls []NotifyCall
}

type NotifyCall struct {
    UserID  int
    Message string
}

func (f *FakeNotifier) Notify(userID int, message string) error {
    f.Calls = append(f.Calls, NotifyCall{userID, message})
    return nil
}

Then assert:

if len(notifier.Calls) != 1 {
    t.Errorf("expected 1 call, got %d", len(notifier.Calls))
}
if notifier.Calls[0].UserID != 1 {
    t.Errorf("wrong userID: %d", notifier.Calls[0].UserID)
}

Error Injection

Make errors configurable:

type FakeRepo struct {
    GetErr  error
    SaveErr error
}

func (f *FakeRepo) GetByID(id int) (*User, error) {
    if f.GetErr != nil {
        return nil, f.GetErr
    }
    return &User{ID: id}, nil
}

Test error paths:

repo := &FakeRepo{GetErr: sql.ErrNoRows}
// test handles error correctly

The Comparison

FeatureMoq/NSubstituteGo Fakestestify/mock
BoilerplateNoneManualSome
Setup syntaxFluentManualMethod calls
VerificationBuilt-inManualBuilt-in
Type safetyGoodExcellentOkay (strings)
DebuggingProxy magicClear codeSome magic
Learning curveModerateLowLow
GenerationNot neededOptionalOptional

When to Use What

Hand-written fakes when:

  • Interface is small (1-3 methods)
  • You need the fake in multiple tests
  • You want maximum clarity
  • You prefer no dependencies

testify/mock when:

  • Interface is larger
  • You need verification of call counts/order
  • You’re comfortable with the DSL
  • You want Moq-like experience

Generated mocks when:

  • Interface is large and changes frequently
  • You have many interfaces to mock
  • You want consistency across the codebase

The Honest Take

I thought I’d miss Moq. I don’t, really.

What Go does better:

  • No mock magic to debug
  • Fakes are real code you can step through
  • Small interfaces mean small fakes
  • No framework dependency

What C# does better:

  • Zero boilerplate for any interface
  • Powerful verification syntax
  • Better for large interfaces
  • Established patterns and tooling

The verdict: Go’s approach is more work for each interface, but that work is simple and debuggable. If you design with small interfaces (as Go encourages), the fake-writing burden is minimal.

Start with hand-written fakes. Reach for testify/mock if you find yourself writing the same patterns repeatedly. Use generated mocks for genuinely large interfaces.

The bigger lesson: good interface design reduces mocking complexity in any language. Go just makes that more obvious.


Next up: benchmarks and profiling—go test -bench and pprof for understanding where your performance actually goes.