go-add-test¶
Add unit/integration tests to a GOB Go microservice following established project patterns.
Triggers¶
- "add test", "add tests", "criar teste", "novo teste", "test coverage", "testar"
- "write tests for", "escrever testes para"
- Examples: "add tests for process-service service layer", "criar testes de domain para bulletin-service"
Instructions¶
You generate Go tests for GOB microservices following the MockFunc pattern established in gob-member-service and gob-auth-service.
Step 1: Identify Target¶
Parse the user request to determine:
- Service: which service in services/ (e.g., gob-process-service)
- Layer: which layer to test: domain, service, handler, repository, consumer
- Scope: specific file/method or entire layer
If not specified, ask the user.
Step 2: Pre-flight Reads¶
Before writing ANY test code, read these files in the target service:
- Repository interfaces —
internal/repository/repository.goor similar interface files - These define the methods you need to mock
- Target source file — the file being tested (e.g.,
internal/service/member_service.go) - Understand every method signature, dependencies, and error paths
- Domain structs —
internal/domain/*.go - Needed for creating test fixtures
- Existing tests — any
*_test.gofiles in the target package - Follow existing patterns if tests already exist
- DTO structs —
internal/dto/dto.goif testing service/handler layer - Request/response types used by the methods
Step 3: Generate Mocks (if mocks_test.go doesn't exist)¶
Create mocks_test.go in the same package as the tests. Use the MockFunc pattern:
package service
import (
"context"
"github.com/google/uuid"
"github.com/gob/{service}/internal/domain"
)
// --- MockXxxRepository ---
type MockXxxRepository struct {
FindByIDFunc func(ctx context.Context, id uuid.UUID) (*domain.Xxx, error)
CreateFunc func(ctx context.Context, entity *domain.Xxx) error
UpdateFunc func(ctx context.Context, entity *domain.Xxx) error
DeleteFunc func(ctx context.Context, id uuid.UUID) error
ListFunc func(ctx context.Context, limit, offset int) ([]*domain.Xxx, int, error)
// Add one field per interface method
}
func (m *MockXxxRepository) FindByID(ctx context.Context, id uuid.UUID) (*domain.Xxx, error) {
if m.FindByIDFunc != nil {
return m.FindByIDFunc(ctx, id)
}
return nil, nil
}
func (m *MockXxxRepository) Create(ctx context.Context, entity *domain.Xxx) error {
if m.CreateFunc != nil {
return m.CreateFunc(ctx, entity)
}
return nil
}
// ... implement ALL interface methods with nil-check + safe default
Rules for mocks:
- One MockXxxRepository struct per repository interface
- Field name = method name + Func suffix
- Safe defaults: return nil, nil for pointer returns, nil for error-only returns
- Mocks live in mocks_test.go in the SAME package (not a separate mocks directory)
- Build tag: none needed (they're _test.go files)
Step 4: Generate Setup Helper¶
Create a setup function that wires the service with all mock dependencies:
func setupXxxService() (*XxxService, *MockAaaRepository, *MockBbbRepository) {
aaaRepo := &MockAaaRepository{}
bbbRepo := &MockBbbRepository{}
// Match the actual constructor signature
service := NewXxxService(aaaRepo, bbbRepo)
// OR if the service uses a Repositories struct:
// repos := &Repositories{aaa: aaaRepo, bbb: bbbRepo}
// service := NewXxxService(repos)
return service, aaaRepo, bbbRepo
}
Rules:
- Return the service AND all individual mock repos (so tests can configure Func fields)
- Match the real constructor — check NewXxxService() signature
- If the service takes a config, create a testConfig() helper too
Step 5: Generate Tests¶
Domain Tests (internal/domain/*_test.go)¶
Pure unit tests — no mocks needed:
func TestXxx_MethodName(t *testing.T) {
tests := []struct {
name string
// input fields
wantErr error
}{
{
name: "valid case",
wantErr: nil,
},
{
name: "invalid - reason",
wantErr: ErrSpecificError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entity := &Xxx{/* fields */}
err := entity.MethodName()
if tt.wantErr != nil {
if err != tt.wantErr {
t.Errorf("MethodName() error = %v, want %v", err, tt.wantErr)
}
return
}
if err != nil {
t.Errorf("MethodName() unexpected error: %v", err)
}
})
}
}
Service Tests (internal/service/*_test.go)¶
func TestXxxService_Create(t *testing.T) {
service, xxxRepo, _ := setupXxxService()
ctx := context.Background()
t.Run("success", func(t *testing.T) {
xxxRepo.CreateFunc = func(ctx context.Context, entity *domain.Xxx) error {
return nil
}
req := &dto.CreateXxxRequest{
Name: "Test",
}
result, err := service.Create(ctx, req)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if result.Name != "Test" {
t.Errorf("Create() Name = %v, want %v", result.Name, "Test")
}
})
t.Run("validation error - empty name", func(t *testing.T) {
req := &dto.CreateXxxRequest{
Name: "",
}
_, err := service.Create(ctx, req)
if err == nil {
t.Error("Create() expected validation error")
}
})
t.Run("repository error", func(t *testing.T) {
xxxRepo.CreateFunc = func(ctx context.Context, entity *domain.Xxx) error {
return errors.New("db connection failed")
}
req := &dto.CreateXxxRequest{
Name: "Test",
}
_, err := service.Create(ctx, req)
if err == nil {
t.Error("Create() expected error when repo fails")
}
})
}
Handler Tests (internal/handler/*_test.go)¶
func TestXxxHandler_Create(t *testing.T) {
app := fiber.New()
mockService := &MockXxxService{}
handler := NewXxxHandler(mockService)
app.Post("/xxx", handler.Create)
t.Run("success - 201", func(t *testing.T) {
mockService.CreateFunc = func(ctx context.Context, req *dto.CreateXxxRequest) (*dto.XxxResponse, error) {
return &dto.XxxResponse{ID: uuid.New(), Name: req.Name}, nil
}
body := `{"name": "Test"}`
req := httptest.NewRequest("POST", "/xxx", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test() error = %v", err)
}
if resp.StatusCode != fiber.StatusCreated {
t.Errorf("status = %d, want %d", resp.StatusCode, fiber.StatusCreated)
}
})
t.Run("bad request - invalid JSON", func(t *testing.T) {
req := httptest.NewRequest("POST", "/xxx", strings.NewReader("{invalid"))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test() error = %v", err)
}
if resp.StatusCode != fiber.StatusBadRequest {
t.Errorf("status = %d, want %d", resp.StatusCode, fiber.StatusBadRequest)
}
})
}
Consumer/Messaging Tests (internal/messaging/*_test.go)¶
func TestProcessHandler_HandleEvent(t *testing.T) {
mockService := &MockXxxService{}
handler := NewProcessHandler(mockService)
t.Run("process.created event", func(t *testing.T) {
var called bool
mockService.HandleProcessCreatedFunc = func(ctx context.Context, event *domain.ProcessEvent) error {
called = true
return nil
}
payload := []byte(`{"process_id": "` + uuid.New().String() + `", "action": "created"}`)
err := handler.Handle(context.Background(), payload)
if err != nil {
t.Fatalf("Handle() error = %v", err)
}
if !called {
t.Error("HandleProcessCreated was not called")
}
})
}
Step 6: Test Coverage Cases¶
For EVERY method tested, always include these scenarios:
- Happy path — valid input, expected output
- Validation errors — missing required fields, invalid values
- Not found — entity doesn't exist (return nil from repo)
- Repository/dependency errors — database failures, external service failures
- Authorization (if applicable) — wrong scope, missing permissions
- Edge cases — empty lists, zero values, UUID nil, boundary values
Conventions¶
| Rule | Value |
|---|---|
| Test framework | Standard testing package only (NO testify) |
| Mock library | Hand-written MockFunc pattern (NO mockgen, gomock) |
| Test file location | Same package as source (_test suffix) |
| Mock file | mocks_test.go per package |
| Naming | Test{Type}_{Method} (e.g., TestMemberService_Create) |
| Structure | Table-driven with t.Run() subtests |
| Assertions | if got != want { t.Errorf(...) } |
| Fatal vs Error | t.Fatalf for setup failures, t.Errorf for assertion failures |
| Context | Always use context.Background() |
| UUIDs | Use uuid.New() for test IDs |
| Time | Use time.Now() or fixed time.Date() for deterministic tests |
Anti-patterns to AVOID¶
- Do NOT use
github.com/stretchr/testify - Do NOT generate mocks with
mockgenor any code generator - Do NOT put mocks in a separate
mocks/directory - Do NOT use
reflect.DeepEqual— compare fields individually - Do NOT test private functions directly — test through public API
- Do NOT create
TestMainunless writing integration tests with DB - Do NOT add build tags for unit tests
Verification¶
After generating tests, run:
If compilation fails, fix immediately. Common issues: - Missing mock method (interface not fully implemented) - Wrong import path - Struct field mismatch (check domain structs again) - Constructor signature changed
Reference Services¶
- Best example:
services/gob-member-service/internal/service/— full MockFunc pattern, setup helpers, table-driven tests - Auth patterns:
services/gob-auth-service/internal/service/— in-memory store mocks, complex scenario tests - Domain only:
services/gob-election-service/internal/domain/— pure domain validation tests