Machine generated documentation. Contribute to improve quality together.

PREVIEW

Functionality in this post is in consideration and is not currently under development.

Testing Your API with Generated Mock Clients in gohandlers

When building an HTTP API, it’s crucial to test the layers around your HTTP handlers: the business logic, the services, the integrations. But if your code depends on a real HTTP client to call your own API, testing can get messy and slow—requiring servers, requests, responses, and a lot of setup.

That’s where gohandlers shines again. It doesn’t just generate server-side code and typed HTTP clients—it also creates mock clients, so you can test your services quickly and deterministically, with no need to spin up HTTP servers or stub request objects.

In this article, you’ll learn how to use gohandlers’ generated mocks to write fast, focused, and maintainable tests for any code that uses your API clients.


🧪 Why Use a Mock Client?

Imagine you have a service that relies on an API client to call another module or service in your system:

type PetService struct {
  Client client.Interface
}

func (s *PetService) RegisterPet(ctx context.Context, name, tag string) (string, error) {
  resp, err := s.Client.CreatePet(ctx, &client.CreatePetRequest{
    Name: name,
    Tag:  tag,
  })
  if err != nil {
    return "", err
  }
  return resp.ID, nil
}

How do you test this without calling the real API?

You use a mock client—one that pretends to be the real thing, but behaves how you want in your test.


⚙️ Generating Mocks

To generate mocks for your typed API client, run:

gohandlers mock \
  --dir handlers/pets \
  --pkg client \
  --out mock.gh.go \
  --v

This will generate:


🧰 Example: Unit Testing with a Mock

Here’s how you’d test the PetService example from earlier:

func TestRegisterPet(t *testing.T) {
  mock := &client.MockClient{}
  mock.CreatePetFunc = func(ctx context.Context, req *client.CreatePetRequest) (*client.CreatePetResponse, error) {
    if req.Name == "" {
      return nil, errors.New("name is required")
    }
    return &client.CreatePetResponse{ID: "mock123"}, nil
  }

  service := &PetService{Client: mock}

  id, err := service.RegisterPet(context.Background(), "Whiskers", "cat")
  if err != nil {
    t.Fatalf("unexpected error: %v", err)
  }
  if id != "mock123" {
    t.Errorf("unexpected ID: got %s, want mock123", id)
  }
}

✅ No network.
✅ No JSON marshalling.
✅ No HTTP server.
✅ 100% pure logic test.


📐 Mock Signature

Each method on the mock client has this pattern:

type MockClient struct {
  CreatePetFunc func(ctx context.Context, req *CreatePetRequest) (*CreatePetResponse, error)
}

If you don’t assign the function, calling the method will panic with a helpful message like:

panic: mock CreatePetFunc not implemented

This ensures you never silently skip behavior in tests.


🎛 Mocking Edge Cases

You can fully control the behavior of the mock to simulate:

Example:

mock.CreatePetFunc = func(ctx context.Context, req *CreatePetRequest) (*CreatePetResponse, error) {
  return nil, fmt.Errorf("server temporarily unavailable")
}

This lets you test retries, fallbacks, or error handling logic—without hitting a real endpoint.


🔁 Table-Driven Testing with Mocks

Want to test multiple scenarios cleanly? Use table-driven tests:

tests := []struct {
  name string
  setup func(*client.MockClient)
  expectErr bool
}{
  {
    name: "success",
    setup: func(mock *client.MockClient) {
      mock.CreatePetFunc = func(ctx context.Context, req *CreatePetRequest) (*CreatePetResponse, error) {
        return &CreatePetResponse{ID: "abc"}, nil
      }
    },
    expectErr: false,
  },
  {
    name: "empty name",
    setup: func(mock *client.MockClient) {
      mock.CreatePetFunc = func(ctx context.Context, req *CreatePetRequest) (*CreatePetResponse, error) {
        return nil, errors.New("name is required")
      }
    },
    expectErr: true,
  },
}

for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
    mock := &client.MockClient{}
    tt.setup(mock)

    service := &PetService{Client: mock}
    _, err := service.RegisterPet(context.Background(), "", "dog")

    if tt.expectErr && err == nil {
      t.Errorf("expected error but got nil")
    }
  })
}

This pattern keeps your test logic tidy and expressive.


🧠 Why Mocks Are Better Than Test Servers

With MockClient With Test HTTP Server
No actual HTTP traffic Requires httptest.Server
Fast and deterministic Can be slower and flaky
Full control over behavior Must parse requests, encode responses
Statically typed interactions Often uses interface{} or raw JSON
Zero setup or teardown Needs cleanup and coordination

Mocks are ideal when testing how your code behaves, not the behavior of the HTTP infrastructure.


🧼 Best Practices


✅ Summary

gohandlers-generated mock clients help you write tests that are:

You don’t need to write mocks by hand, and you don’t need to simulate full HTTP interactions. With gohandlers, testing your API-consuming code is just as clean as writing it.

Code with confidence. Test with ease. 🧪✅🚀