| name | testing-strategy |
| description | Test patterns, mocking strategies, and organization best practices |
| version | 1.0.0 |
| category | development |
| tags | ["testing","unit-test","integration-test","mock","testify"] |
| author | ZGO Team |
| updated | "2026-01-24T00:00:00.000Z" |
Testing Strategy
📋 Purpose
This skill defines the testing standards and strategies for the ZGO project. It ensures that code is reliable, maintainable, and verifiable through various testing tiers.
🎯 When to Use
- Writing unit tests for services and repositories
- Implementing integration tests for API flows
- Mocking external dependencies
- Organizing test suites in a new module
- Improving test coverage and reliability
⚙️ Prerequisites
🏗️ Testing Tiers
1. Unit Tests (Level 1)
- Scope: Single function or method.
- Speed: Very fast (milliseconds).
- Dependencies: All external dependencies (DB, APIs, other services) MUST be mocked.
- Location: Same package as the code being tested (
*_test.go).
2. Integration Tests (Level 2)
- Scope: Interaction between multiple components or layers.
- Speed: Moderate.
- Dependencies: May use real lightweight dependencies (e.g., SQLite/PostgreSQL in Docker) or high-fidelity mocks.
- Location: Often in
tests/integration/ or package-specific integration subdirectories.
3. E2E / API Tests (Level 3)
- Scope: Full request-response cycle.
- Speed: Slow.
- Dependencies: Real or near-real environment.
- Location:
tests/api/ or tests/e2e/.
🧪 Unit Testing Patterns
1. Table-Driven Tests (REQUIRED)
Table-driven tests are the standard in Go. They allow testing multiple scenarios with minimal boilerplate.
func TestCalculateTotal(t *testing.T) {
tests := []struct {
name string
items []float64
tax float64
expected float64
}{
{"empty items", []float64{}, 0.1, 0},
{"single item", []float64{100}, 0.1, 110},
{"multiple items", []float64{10, 20, 30}, 0.05, 63},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CalculateTotal(tt.items, tt.tax)
assert.Equal(t, tt.expected, result)
})
}
}
2. File Naming and Package
- File:
{original_file}_test.go
- Package: Same as the file being tested (e.g.,
package user) or package user_test for external black-box testing.
🎭 Mocking Strategy
We use github.com/stretchr/testify/mock for dependency injection.
1. Defining Mocks
Mocks should implement the interface of the dependency they replace.
type MockRepository struct {
mock.Mock
}
func (m *MockRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*domain.User), args.Error(1)
}
2. Setting Expectations
func TestService_GetByID(t *testing.T) {
mockRepo := new(MockRepository)
service := NewService(mockRepo)
ctx := context.Background()
expectedUser := &domain.User{ID: 1, Username: "stark"}
mockRepo.On("GetByID", ctx, uint(1)).Return(expectedUser, nil)
user, err := service.GetByID(ctx, 1)
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
mockRepo.AssertExpectations(t)
}
🔗 Integration Testing
Integration tests verify that components work together.
1. Database Integration
Use a test database (SQLite in-memory or a dedicated PostgreSQL container).
func TestRepository_Create(t *testing.T) {
db := setupTestDB()
repo := NewRepository(db)
user := &domain.User{Username: "testuser", Email: "test@example.com"}
err := repo.Create(context.Background(), user)
assert.NoError(t, err)
assert.NotZero(t, user.ID)
}
2. API Integration
Use net/http/httptest to test handlers without starting a real server.
func TestHandler_Create(t *testing.T) {
gin.SetMode(gin.TestMode)
mockService := new(MockService)
handler := NewHandler(mockService)
r := gin.Default()
r.POST("/users", handler.Create)
body := `{"username": "test", "email": "test@example.com"}`
req, _ := http.NewRequest("POST", "/users", strings.NewReader(body))
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
}
📋 Best Practices
- Assert with
testify: Use assert for non-terminal failures and require for failures that should stop the test immediately.
- Clean State: Each test Run must be independent. Reset mocks and clear test databases between runs.
- Naming: Test functions must start with
Test. Sub-tests (in t.Run) should have descriptive names.
- No Side Effects: Tests should not modify shared resources (like global variables) unless properly synchronized or reset.
- Coverage Goals: Focus on business logic in Services and critical paths in Repositories. Aim for >80% coverage in these areas.
🔧 Automation & Tools
Running Tests
make test
go test ./internal/modules/user/...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Mock Generation
We use mockery for automatic mock generation.
mockery --all --dir=internal/modules/user
✅ Verification Checklist
📚 Complete Examples
🔗 Related Skills
Version: 1.0.0
Last Updated: 2026-01-24
Maintainer: ZGO Team