| name | go-add-test |
| description | Add unit or integration tests to Go microservices using established project patterns. Use when Codex needs to write Go tests, mocks, fixtures, or coverage for service, domain, handler, repository, or consumer layers. |
CSTK Adaptation
Adapted from JotJunior/cstk skill go-add-test at commit 35922cf.
Use this as a Codex skill; follow local repository instructions and Codex tool names when source text mentions Claude-specific commands or slash commands.
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.go or 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.go files in the target package
- Follow existing patterns if tests already exist
- DTO structs —
internal/dto/dto.go if 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"
)
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)
}
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
}
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{}
service := NewXxxService(aaaRepo, bbbRepo)
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
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{}
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
mockgen or 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
TestMain unless writing integration tests with DB
- Do NOT add build tags for unit tests
Verification
After generating tests, run:
cd services/{service-name} && go test ./internal/{layer}/... -v -count=1
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
Source License
This skill includes material adapted from JotJunior/cstk, licensed under MIT. The copyright and permission notice are included in references/cstk-license.md.