8.9 KiB
Testing Patterns
Analysis Date: 2026-03-21
Test Framework
Runner:
- Go standard
testingpackage - Config:
Makefiletargettest
Assertion Libraries:
github.com/stretchr/testify v1.11.1—assertpackage (preferred in newer tests)- Standard
t.Fatal,t.Fatalf,t.Errorf(used in most tests, older style)
Mocking Libraries:
github.com/DATA-DOG/go-sqlmock v1.5.2— SQL-level DB mockinggithub.com/alicebob/miniredis/v2 v2.36.1— in-process Redis mock server- Manual mock structs implementing interfaces (for
core.Context, logger)
Run Commands:
make test # Run all tests: go test -v --cover ./internal/...
go test -v ./internal/service/... # Test specific package
go test -v -run TestFunctionName ./... # Run single test
Coverage output: coverage.out (present in repo root)
Test File Organization
Location:
- Co-located with source files —
foo_test.gosits in the same directory asfoo.go - No separate
tests/directory for Go unit/integration tests
Naming:
<subject>_test.gomatching the function/feature under test- Package declaration: same package as source (
package activity) for white-box tests, orpackage game_testfor black-box tests (rare)
Structure:
internal/service/activity/
├── activity_order_service.go
├── concurrency_test.go # integration (real DB)
├── reward_snapshot_test.go # integration (SQLite in-memory)
├── sanitize_test.go # unit (no DB)
internal/service/game/
├── token.go
├── token_test.go # uses miniredis + SQLite
internal/service/user/
├── error_test.go # uses go-sqlmock
├── request_shipping_batch_test.go
internal/api/app/
├── store_test.go # HTTP handler integration test
Test Structure
Suite Organization (table-driven tests):
func TestShouldTriggerInstantDraw(t *testing.T) {
testCases := []struct {
name string
orderStatus int32
drawMode string
shouldTrigger bool
}{
{"已支付+即时开奖", 2, "instant", true},
{"已支付+定时开奖", 2, "scheduled", false},
{"未支付+即时开奖", 1, "instant", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := shouldTriggerInstantDraw(tc.orderStatus, tc.drawMode)
if result != tc.shouldTrigger {
t.Errorf("期望触发=%v,实际触发=%v", tc.shouldTrigger, result)
}
})
}
}
Patterns:
- Setup: create repo/DB then initialize service via constructor
- Teardown: SQLite in-memory DBs are ephemeral (no cleanup needed); miniredis uses
defer mr.Close() - Assertion:
t.Fatalffor unrecoverable setup failures;t.Errorffor assertion failures - Skipping:
t.Skipfwhen preconditions absent (e.g., real DB unavailable in concurrency tests)
Mocking
In-memory SQLite (primary DB mock):
repo, err := mysql.NewSQLiteRepoForTest() // internal/repository/mysql/testrepo_sqlite.go
if err != nil {
t.Fatal(err)
}
db := repo.GetDbW()
// Manually create tables with SQLite-compatible DDL
db.Exec(`CREATE TABLE products (id INTEGER PRIMARY KEY AUTOINCREMENT, ...)`)
NewSQLiteRepoForTest() → gorm.Open(sqlite.Open(":memory:"), ...) → returns Repo interface.
TestRepo wrapping real DB (for integration tests with live MySQL):
db, err := gorm.Open(drivermysql.Open(dsn), &gorm.Config{})
repo := mysql.NewTestRepo(db) // internal/repository/mysql/test_helper.go
go-sqlmock (SQL-level mocking):
db, mock, err := sqlmock.New()
gormDB, _ := gorm.Open(gormmysql.New(gormmysql.Config{
Conn: db,
SkipInitializeWithVersion: true,
}), &gorm.Config{})
mock.ExpectQuery("SELECT .* FROM `system_item_cards`").
WithArgs(100, sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"id", "status"}).AddRow(100, 0))
miniredis (Redis mock):
mr, err := miniredis.Run()
defer mr.Close()
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
Manual interface mock (for core.Context):
type mockContext struct {
core.Context // embed to satisfy interface
ctx context.Context
}
func (m *mockContext) RequestContext() core.StdContext { return core.StdContext{Context: m.ctx} }
func (m *mockContext) ShouldBindJSON(obj interface{}) error { return nil }
func (m *mockContext) AbortWithError(err core.BusinessError) {}
// ... implement all interface methods with no-op stubs
MockLogger pattern:
type MockLogger struct {
logger.CustomLogger
}
func (l *MockLogger) Info(msg string, fields ...zap.Field) {}
func (l *MockLogger) Error(msg string, fields ...zap.Field) {}
func (l *MockLogger) Warn(msg string, fields ...zap.Field) {}
func (l *MockLogger) Debug(msg string, fields ...zap.Field) {}
What to Mock:
- DB connection (use SQLite in-memory or go-sqlmock)
- Redis (use miniredis)
- External API clients (use interface injection)
- Logger (use MockLogger embedding
logger.CustomLogger) core.Context(use manual mock struct)
What NOT to Mock:
- Business logic within services under test
- DAO query building when testing DB query behavior
Fixtures and Factories
Test Data (DDL + seed SQL directly in test):
// Create table
repo.GetDbW().Exec(`CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
status INTEGER NOT NULL DEFAULT 1,
...
)`)
// Seed data
db.Exec("INSERT INTO orders (id, user_id, status, source_type, total_amount, created_at) VALUES (101, ?, 2, 0, 100, ?)", userID, o1Time)
Shared table initialization helpers:
func initTestTables(t *testing.T, db *gorm.DB) { ... } // in task_center package tests
func ensureExtraTablesForServiceTest(t *testing.T, db *gorm.DB) { ... }
Test helper functions marked with t.Helper():
func assertAttribution(t *testing.T, got map[int64]activityAttribution, activityID, wantChannelID int64, wantChannelCode string) {
t.Helper()
...
}
Location:
- No centralized fixtures directory. Each test file creates its own data inline.
- Shared helpers defined within the same package test files.
Coverage
Requirements: make test runs with --cover flag but no enforced minimum threshold.
View Coverage:
go test -v --cover ./internal/... # prints coverage % per package
Coverage output file: /Users/win/2025/AICoding/bindbox/bindbox_game/coverage.out
Test Types
Unit Tests (pure logic, no DB):
- Scope: individual pure functions — JSON utilities, string parsing, coupon discount math
- Pattern: call function, assert return value
- Examples:
internal/service/product/product_test.go(TestNormalizeJSON, TestSplitImages),internal/service/order/discount_test.go(TestApplyCouponDiscount),internal/service/activity/sanitize_test.go
Integration Tests (SQLite in-memory DB):
- Scope: service methods requiring DB reads/writes, HTTP handler end-to-end via
net/http/httptest - Pattern:
NewSQLiteRepoForTest()→ create DDL → seed data → call service/handler → assert DB state or response - Examples:
internal/service/task_center/service_test.go,internal/api/app/store_test.go,internal/service/activity/reward_snapshot_test.go
Integration Tests (Real MySQL — skipped when unavailable):
- Scope: concurrency/race condition testing requiring real DB transactions
- Pattern: hardcoded DSN,
t.Skipfon connection failure - Examples:
internal/service/activity/concurrency_test.go
E2E Tests: Not present in the current codebase.
Common Patterns
HTTP Handler Testing:
mux, _ := core.New(lg)
mux.Group("/api/app").GET("/store/items", NewStore(lg, repo).ListStoreItemsForApp())
rr := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/app/store/items?kind=product&page=1&page_size=10", bytes.NewBufferString(""))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("code=%d body=%s", rr.Code, rr.Body.String())
}
var rsp map[string]interface{}
json.Unmarshal([]byte(rr.Body.String()), &rsp)
Async/Concurrency Testing:
var wg sync.WaitGroup
var mu sync.Mutex
successCount := 0
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
// ... concurrent operation
mu.Lock()
defer mu.Unlock()
successCount++
}(i)
}
wg.Wait()
// assert final DB state
Error Path Testing:
err := svc.AddItemCard(context.Background(), 1, 100, 1)
assert.Error(t, err)
assert.Equal(t, "record not found", err.Error())
Testify assertion style (preferred in newer tests):
assert.NoError(t, err)
assert.NotEmpty(t, token)
assert.Equal(t, userID, claims.UserID)
assert.Contains(t, err.Error(), "invalid ticket format")
Testing analysis: 2026-03-21