Test
Guide for writing effective tests using best practices: test pyramid, Arrange-Act-Assert pattern, mocking strategies, and coverage guidelines. Use when the user asks to write tests, improve test coverage, refactor tests, or understand testing best practices. Do NOT use when the user wants to run existing tests without modification, or when the focus is on test infrastructure setup (CI/CD, test frameworks installation).From its SKILL.md
npx -y skills add marco-souza/skills --skill testAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.8 KB, ~3.2k tokens by cl100k_base, as published. Nobody here has run it
Testing Best Practices
Write reliable, maintainable tests that give confidence in code correctness.
When to Use
- Writing new tests for features or bug fixes
- Improving test coverage in under-tested areas
- Refactoring flaky or brittle tests
- Reviewing test quality during code review
- Adding regression tests for discovered bugs
When NOT to Use
- Running tests without modifications
- Setting up test infrastructure (CI/CD, framework installation)
- Performance/load testing (use dedicated load testing tools)
Test Pyramid
Structure your test suite using the test pyramid model:
/\
/ \ Unit Tests (Fast, many)
/ \
/------\
/ \ Integration Tests (Medium, fewer)
/----------\
/ \ E2E Tests (Slow, few)
/--------------\
Unit Tests (Base Layer)
- Purpose: Test individual functions/methods in isolation
- Speed: Milliseconds
- Quantity: Most tests should be unit tests
- Isolation: No external dependencies (network, database, filesystem)
// Good: Pure unit test, no dependencies
func TestCalculateTotal(t *testing.T) {
items := []Item{
{Price: 10.00, Qty: 2},
{Price: 5.00, Qty: 1},
}
got := CalculateTotal(items)
want := 25.00
if got != want {
t.Errorf("CalculateTotal() = %v, want %v", got, want)
}
}
Integration Tests (Middle Layer)
- Purpose: Test component interactions and external integrations
- Speed: Seconds
- Quantity: Moderate
- Isolation: May use test databases, mock external APIs
// Integration test with database
func TestUserRepository_Create(t *testing.T) {
db := setupTestDB(t)
defer cleanupTestDB(t, db)
repo := NewUserRepository(db)
user := &User{Name: "Alice", Email: "[email protected]"}
err := repo.Create(user)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if user.ID == 0 {
t.Error("expected user ID to be set")
}
}
End-to-End Tests (Top Layer)
- Purpose: Test complete user workflows through the full stack
- Speed: Seconds to minutes
- Quantity: Few, focused on critical paths
- Isolation: Full environment required
// E2E test hitting real HTTP endpoint
func TestLoginEndpoint(t *testing.T) {
server := startTestServer(t)
defer server.Close()
resp, err := http.Post(
server.URL+"/api/login",
"application/json",
strings.NewReader(`{"email":"[email protected]","password":"secret"}`),
)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
}
Arrange-Act-Assert Pattern
Structure every test with three clear phases:
func TestDiscountCalculation(t *testing.T) {
// ARRANGE: Set up test data and dependencies
product := &Product{Price: 100.00}
discount := 0.20 // 20% off
// ACT: Execute the function under test
result := CalculateDiscount(product.Price, discount)
// ASSERT: Verify the expected outcome
want := 80.00
if result != want {
t.Errorf("CalculateDiscount() = %v, want %v", result, want)
}
}
Why AAA?
- Readability: Clear separation of concerns
- Maintainability: Easy to identify which phase breaks
- Documentation: Test intent is obvious
AAA Variants
// Given-When-Then (BDD style)
func TestLoginValidation(t *testing.T) {
// Given: valid credentials
credentials := Credentials{Email: "[email protected]", Password: "valid123"}
// When: user attempts login
result, err := AuthService.Login(credentials)
// Then: login succeeds
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result.Token == "" {
t.Error("expected token to be set")
}
}
// Arrange-Act-Assert-Cleanup (for resources)
func TestFileProcessing(t *testing.T) {
// Arrange
tmpFile := createTempFile(t, "test data")
defer os.Remove(tmpFile)
// Act
result, err := ProcessFile(tmpFile)
// Assert
if err != nil {
t.Fatalf("ProcessFile() error = %v", err)
}
if result.Processed != true {
t.Error("expected file to be marked as processed")
}
}
Mocking and Stubbing
When to Mock
- External services (APIs, databases, file systems)
- Time-dependent code
- Random/non-deterministic values
- Slow operations
Interface-Based Mocking (Go)
Define interfaces for dependencies:
// Production code depends on interface, not concrete type
type UserStore interface {
GetByID(id int64) (*User, error)
Save(user *User) error
}
// Mock implementation
type MockUserStore struct {
Users map[int64]*User
SaveFn func(user *User) error
}
func (m *MockUserStore) GetByID(id int64) (*User, error) {
user, ok := m.Users[id]
if !ok {
return nil, ErrNotFound
}
return user, nil
}
func (m *MockUserStore) Save(user *User) error {
if m.SaveFn != nil {
return m.SaveFn(user)
}
m.Users[user.ID] = user
return nil
}
Using Mocks in Tests
func TestGetUserProfile(t *testing.T) {
// Arrange
mockStore := &MockUserStore{
Users: map[int64]*User{
1: {ID: 1, Name: "Alice", Email: "[email protected]"},
},
}
service := NewUserService(mockStore)
// Act
profile, err := service.GetProfile(1)
// Assert
if err != nil {
t.Fatalf("GetProfile() error = %v", err)
}
if profile.Name != "Alice" {
t.Errorf("Name = %q, want %q", profile.Name, "Alice")
}
}
func TestSaveUser_RejectsInvalidEmail(t *testing.T) {
// Arrange
mockStore := &MockUserStore{
Users: make(map[int64]*User),
}
service := NewUserService(mockStore)
user := &User{Name: "Bob", Email: "invalid"}
// Act
err := service.Save(user)
// Assert
if err == nil {
t.Error("expected error for invalid email, got nil")
}
}
Table-Driven Tests
For testing multiple cases efficiently:
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{name: "valid email", email: "[email protected]", wantErr: false},
{name: "missing @", email: "aliceexample.com", wantErr: true},
{name: "missing domain", email: "alice@", wantErr: true},
{name: "empty string", email: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
tt.email, err, tt.wantErr)
}
})
}
}
Coverage Guidelines
Coverage Targets
| Component Type | Minimum Coverage | Rationale |
|---|---|---|
| Business logic | 80-90% | Core correctness |
| Utilities/helpers | 90%+ | Reused everywhere |
| API handlers | 70-80% | HTTP concerns |
| UI components | 50-70% | Harder to test, visual |
| Configuration | 30-50% | Often trivial |
What to Test
Always test:
- Happy paths (expected inputs)
- Error cases (invalid inputs, edge cases)
- Boundary conditions (empty, nil, max values)
- State transitions
Don't obsess over:
- Getters/setters
- Framework boilerplate
- Third-party library internals
Coverage in Go
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View coverage in terminal
go tool cover -func=coverage.out
# Generate HTML report for detailed view
go tool cover -html=coverage.out -o coverage.html
Reading Coverage Output
ok github.com/example/pkg 0.5s coverage: 85.2% of statements
Focus on increasing coverage for files with lowest percentages:
# Show coverage per function
go tool cover -func=coverage.out | sort -k3 -n
Test Organization
File Structure
src/
├── user.go # Production code
├── user_test.go # Unit tests (same package)
├── user_integration_test.go # Integration tests
└── testdata/
└── fixtures/ # Test data files
Naming Conventions
// Function under test: CalculateDiscount
// Standard test (success case)
func TestCalculateDiscount(t *testing.T) { ... }
// Failure/edge case
func TestCalculateDiscount_ZeroDiscount(t *testing.T) { ... }
func TestCalculateDiscount_NegativeAmount(t *testing.T) { ... }
// Benchmark
func BenchmarkCalculateDiscount(b *testing.B) { ... }
// Example (documentation)
func ExampleCalculateDiscount() { ... }
Edge Cases to Cover
Nil/Empty Inputs
func TestProcessItems_EmptySlice(t *testing.T) {
result := ProcessItems([]Item{})
if len(result) != 0 {
t.Error("expected empty result for empty input")
}
}
func TestProcessItems_NilSlice(t *testing.T) {
result := ProcessItems(nil)
if result != nil {
t.Error("expected nil result for nil input")
}
}
Boundary Values
func TestAgeValidation(t *testing.T) {
tests := []struct {
age int
wantErr bool
}{
{age: 0, wantErr: true}, // Minimum boundary
{age: 1, wantErr: false}, // Just above minimum
{age: 17, wantErr: false}, // Below adult age
{age: 18, wantErr: false}, // Adult threshold
{age: 150, wantErr: false}, // Reasonable maximum
{age: 151, wantErr: true}, // Above reasonable max
}
// ... test loop
}
Concurrent Access
func TestConcurrentAccess(t *testing.T) {
cache := NewCache()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
cache.Set(fmt.Sprintf("key%d", n), n)
_ = cache.Get(fmt.Sprintf("key%d", n))
}(i)
}
wg.Wait()
// Cache should not panic or corrupt data
}
Common Anti-Patterns
Avoid
- Tests that depend on execution order — each test should be independent
- Mocking everything — prefer real implementations when practical
- Testing implementation details — test behavior, not internals
- Sleep-based synchronization — use channels or callbacks
- Ignored errors — always check error returns
Good vs Bad Test
// BAD: Tests implementation detail, brittle
func TestUserRepository_InternalCache(t *testing.T) {
repo := NewUserRepository(db)
repo.cache = map[int64]*User{} // Testing internal state
user := repo.GetUser(1)
if _, exists := repo.cache[1]; !exists {
t.Error("expected cache to be populated")
}
}
// GOOD: Tests observable behavior
func TestUserRepository_GetUser_CachesResult(t *testing.T) {
db := &MockDB{QueryCount: 0}
repo := NewUserRepository(db)
// First call hits database
_, _ = repo.GetUser(1)
if db.QueryCount != 1 {
t.Errorf("expected 1 query, got %d", db.QueryCount)
}
// Second call uses cache (no additional query)
_, _ = repo.GetUser(1)
if db.QueryCount != 1 {
t.Errorf("expected still 1 query, got %d", db.QueryCount)
}
}
Quick Reference
Test Command
go test ./... # Run all tests
go test -v ./... # Verbose output
go test -run TestFunctionName # Run specific test
go test -count=3 ./... # Run 3 times (find flaky tests)
go test -race ./... # Detect race conditions
Test Flags
go test -cover # Show coverage percentage
go test -coverprofile=out.out # Save coverage data
go test -bench=. # Run benchmarks
go test -short # Skip long-running tests
go test -timeout 30s # Fail if test exceeds timeout
Best Practices
- One assertion per concept — each test should verify one behavior
- Descriptive names — test names should explain what's being tested
- Independent tests — no test should depend on another test's state
- Fast feedback — keep unit tests under 100ms each
- Test behavior, not implementation — verify outcomes, not methods called
- Keep tests readable — a test is documentation
- Refactor tests — extract helpers for repeated setup
- Delete dead tests — if code is removed, remove its tests too
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.