Initial commit
This commit is contained in:
689
skills/testing/reference.md
Normal file
689
skills/testing/reference.md
Normal file
@@ -0,0 +1,689 @@
|
||||
# Testing Reference
|
||||
|
||||
Complete guide to Go testing principles and patterns.
|
||||
|
||||
## Core Testing Principles
|
||||
|
||||
### 1. Test Only Public API
|
||||
- **Use `pkg_test` package name** - Forces external perspective
|
||||
- **Test types via constructors** - No direct struct initialization
|
||||
- **No testing private methods** - If you need to test it, make it public or rethink design
|
||||
|
||||
```go
|
||||
// ✅ Good
|
||||
package user_test
|
||||
|
||||
import "github.com/yourorg/project/user"
|
||||
|
||||
func TestService_CreateUser(t *testing.T) {
|
||||
svc, _ := user.NewUserService(repo, notifier)
|
||||
err := svc.CreateUser(ctx, testUser)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Avoid Mocks - Use Real Implementations
|
||||
|
||||
Instead of mocks, use:
|
||||
- **HTTP test servers** (`httptest` package)
|
||||
- **Temp files/directories** (`os.CreateTemp`, `os.MkdirTemp`)
|
||||
- **In-memory databases** (SQLite in-memory, or custom implementations)
|
||||
- **Test implementations** (TestEmailer that writes to buffer)
|
||||
|
||||
**Benefits:**
|
||||
- Tests are more reliable
|
||||
- Tests verify actual behavior
|
||||
- Easier to maintain
|
||||
|
||||
### 3. Coverage Strategy
|
||||
|
||||
**Leaf Types** (self-contained):
|
||||
- **Target**: 100% unit test coverage
|
||||
- **Why**: Core logic must be bulletproof
|
||||
|
||||
**Orchestrating Types** (coordinate others):
|
||||
- **Target**: Integration test coverage
|
||||
- **Why**: Test seams between components
|
||||
|
||||
**Goal**: Most logic in leaf types (easier to test and maintain)
|
||||
|
||||
---
|
||||
|
||||
## Table-Driven Tests
|
||||
|
||||
### When to Use
|
||||
- Each test case has **cyclomatic complexity = 1**
|
||||
- No conditionals inside t.Run()
|
||||
- Simple, focused testing scenarios
|
||||
|
||||
### ❌ Anti-Pattern: wantErr bool
|
||||
|
||||
**DO NOT** use `wantErr bool` pattern - it violates complexity = 1 rule:
|
||||
|
||||
```go
|
||||
// ❌ BAD - Has conditionals (complexity > 1)
|
||||
func TestNewUserID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want UserID
|
||||
wantErr bool // ❌ Anti-pattern
|
||||
}{
|
||||
{name: "valid ID", input: "usr_123", want: UserID("usr_123"), wantErr: false},
|
||||
{name: "empty ID", input: "", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NewUserID(tt.input)
|
||||
if tt.wantErr { // ❌ Conditional
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Correct Pattern: Separate Functions
|
||||
|
||||
**Always separate success and error cases:**
|
||||
|
||||
```go
|
||||
// ✅ Success cases - Complexity = 1
|
||||
func TestNewUserID_Success(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want UserID
|
||||
}{
|
||||
{name: "valid ID", input: "usr_123", want: UserID("usr_123")},
|
||||
{name: "with numbers", input: "usr_456", want: UserID("usr_456")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NewUserID(tt.input)
|
||||
require.NoError(t, err) // ✅ No conditionals
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Error cases - Complexity = 1
|
||||
func TestNewUserID_Error(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{name: "empty ID", input: ""},
|
||||
{name: "whitespace only", input: " "},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := NewUserID(tt.input)
|
||||
assert.Error(t, err) // ✅ No conditionals
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Critical Rule: Named Struct Fields
|
||||
|
||||
**ALWAYS use named struct fields** - Linter reorders fields, breaking unnamed initialization:
|
||||
|
||||
```go
|
||||
// ❌ BAD - Breaks when linter reorders fields
|
||||
tests := []struct {
|
||||
name string
|
||||
input int
|
||||
want string
|
||||
}{
|
||||
{"test1", 42, "result"}, // Will break
|
||||
}
|
||||
|
||||
// ✅ GOOD - Works regardless of field order
|
||||
tests := []struct {
|
||||
name string
|
||||
input int
|
||||
want string
|
||||
}{
|
||||
{name: "test1", input: 42, want: "result"}, // Always works
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testify Suites
|
||||
|
||||
### When to Use
|
||||
|
||||
ONLY for complex test infrastructure setup:
|
||||
- Mock HTTP servers
|
||||
- Database connections
|
||||
- OpenTelemetry testing setup
|
||||
- Temporary files/directories needing cleanup
|
||||
- Shared expensive setup/teardown
|
||||
|
||||
### When NOT to Use
|
||||
- Simple unit tests (use table-driven instead)
|
||||
- Tests without complex setup
|
||||
|
||||
### Pattern
|
||||
|
||||
```go
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ServiceSuite struct {
|
||||
suite.Suite
|
||||
server *httptest.Server
|
||||
svc *user.UserService
|
||||
}
|
||||
|
||||
func (s *ServiceSuite) SetupSuite() {
|
||||
s.server = httptest.NewServer(testHandler)
|
||||
}
|
||||
|
||||
func (s *ServiceSuite) TearDownSuite() {
|
||||
s.server.Close()
|
||||
}
|
||||
|
||||
func (s *ServiceSuite) SetupTest() {
|
||||
s.svc = user.NewUserService(s.server.URL)
|
||||
}
|
||||
|
||||
func (s *ServiceSuite) TestCreateUser() {
|
||||
err := s.svc.CreateUser(ctx, testUser)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func TestServiceSuite(t *testing.T) {
|
||||
suite.Run(t, new(ServiceSuite))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Synchronization in Tests
|
||||
|
||||
### Never Use time.Sleep
|
||||
|
||||
Use channels or WaitGroups instead.
|
||||
|
||||
### Use Channels
|
||||
|
||||
```go
|
||||
func TestAsyncOperation(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
doAsyncWork()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Success
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timeout")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use WaitGroups
|
||||
|
||||
```go
|
||||
func TestConcurrentOperations(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
results := make([]string, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
results[index] = doWork(index)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Assert on results
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Organization
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
user/
|
||||
├── user.go
|
||||
├── user_test.go # Tests for user.go (pkg_test)
|
||||
├── service.go
|
||||
├── service_test.go # Tests for service.go (pkg_test)
|
||||
```
|
||||
|
||||
### Package Naming
|
||||
|
||||
```go
|
||||
// ✅ External package - tests public API only
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/yourorg/project/user"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Real Implementation Patterns
|
||||
|
||||
### In-Memory Repository
|
||||
|
||||
```go
|
||||
package user
|
||||
|
||||
type InMemoryRepository struct {
|
||||
mu sync.RWMutex
|
||||
users map[UserID]User
|
||||
}
|
||||
|
||||
func NewInMemoryRepository() *InMemoryRepository {
|
||||
return &InMemoryRepository{
|
||||
users: make(map[UserID]User),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) Save(ctx context.Context, u User) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.users[u.ID] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) Get(ctx context.Context, id UserID) (*User, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
u, ok := r.users[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Test Email Sender
|
||||
|
||||
```go
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TestEmailer struct {
|
||||
mu sync.Mutex
|
||||
buffer bytes.Buffer
|
||||
}
|
||||
|
||||
func NewTestEmailer() *TestEmailer {
|
||||
return &TestEmailer{}
|
||||
}
|
||||
|
||||
func (e *TestEmailer) Send(to Email, subject, body string) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
fmt.Fprintf(&e.buffer, "To: %s\nSubject: %s\n%s\n\n", to, subject, body)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *TestEmailer) SentEmails() string {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return e.buffer.String()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testable Examples (GoDoc Examples)
|
||||
|
||||
### When to Add
|
||||
- Non-trivial types
|
||||
- Types with validation
|
||||
- Common usage patterns
|
||||
|
||||
### Pattern
|
||||
|
||||
```go
|
||||
// Example_UserID demonstrates basic usage.
|
||||
func Example_UserID() {
|
||||
id, _ := user.NewUserID("usr_123")
|
||||
fmt.Println(id)
|
||||
// Output: usr_123
|
||||
}
|
||||
|
||||
// Example_UserID_validation shows validation behavior.
|
||||
func Example_UserID_validation() {
|
||||
_, err := user.NewUserID("")
|
||||
fmt.Println(err != nil)
|
||||
// Output: true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Before Considering Tests Complete
|
||||
|
||||
**Structure:**
|
||||
- [ ] Tests in `pkg_test` package
|
||||
- [ ] Testing public API only
|
||||
- [ ] Table-driven tests use named fields
|
||||
- [ ] No conditionals in test cases
|
||||
|
||||
**Implementation:**
|
||||
- [ ] Using real implementations, not mocks
|
||||
- [ ] No time.Sleep (using channels/waitgroups)
|
||||
- [ ] Testify suites only for complex setup
|
||||
|
||||
**Coverage:**
|
||||
- [ ] Leaf types: 100% unit test coverage
|
||||
- [ ] Orchestrating types: Integration tests
|
||||
- [ ] Happy path, edge cases, error cases covered
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**The Golden Rule**: Cyclomatic complexity = 1 in all test cases
|
||||
|
||||
**Test Structure Choices:**
|
||||
- **Table-driven tests**: Simple, focused scenarios
|
||||
- **Testify suites**: Complex infrastructure setup only
|
||||
|
||||
**Test Philosophy:**
|
||||
- Test only public API (`pkg_test` package)
|
||||
- Use real implementations, not mocks
|
||||
- Leaf types: 100% coverage
|
||||
- Orchestrating types: Integration tests
|
||||
|
||||
**Common Pitfalls to Avoid:**
|
||||
- ❌ Testing private methods
|
||||
- ❌ Heavy mocking
|
||||
- ❌ time.Sleep in tests
|
||||
- ❌ Conditionals in test cases
|
||||
- ❌ Unnamed struct fields in table tests
|
||||
|
||||
---
|
||||
|
||||
# Example Files - Reusable Testing Patterns
|
||||
|
||||
The following example files contain **transferable patterns** that apply to many scenarios, not just the specific technologies shown. Claude should read these files based on the **pattern needed**, not the specific technology mentioned.
|
||||
|
||||
## Pattern 1: In-Memory Test Harness (Level 1)
|
||||
|
||||
**File**: `examples/nats-in-memory.md`
|
||||
|
||||
**Pattern**: Using official test harnesses from Go libraries
|
||||
|
||||
**When to read:**
|
||||
- Need to test with ANY service that provides an official Go test harness
|
||||
- Testing message queues, databases, caches, or any service with in-memory test mode
|
||||
- Want to avoid Docker but need realistic service behavior
|
||||
|
||||
**Applies to:**
|
||||
- **NATS** (shown in example) - Message queue with official test harness
|
||||
- **Redis** - `github.com/alicebob/miniredis` pure Go in-memory Redis
|
||||
- **MongoDB** - `github.com/tryvium-travels/memongo` in-memory MongoDB
|
||||
- **PostgreSQL** - `github.com/jackc/pgx/v5` with pgx mock
|
||||
- **Any Go library with test package** - Check if dependency has `/test` package
|
||||
|
||||
**Key techniques to adapt:**
|
||||
- Wrapping official harness with clean API
|
||||
- Free port allocation for parallel tests
|
||||
- Clean lifecycle management (Setup/Teardown)
|
||||
- Thread-safe initialization
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Binary Dependency Management (Level 2)
|
||||
|
||||
**File**: `examples/victoria-metrics.md`
|
||||
|
||||
**Pattern**: Download, manage, and run ANY standalone binary for testing
|
||||
|
||||
**When to read:**
|
||||
- Need to test against ANY external binary executable
|
||||
- No in-memory option available
|
||||
- Want production-like testing without Docker
|
||||
|
||||
**Applies to:**
|
||||
- **Victoria Metrics** (shown in example) - Metrics database
|
||||
- **Prometheus** - Metrics and alerting
|
||||
- **Grafana** - Dashboards and visualization
|
||||
- **Any database binaries** - PostgreSQL, MySQL, Redis, etc.
|
||||
- **Any CLI tools** - Language servers, formatters, linters
|
||||
- **Custom binaries** - Your own services or third-party tools
|
||||
|
||||
**Key techniques to adapt:**
|
||||
- OS/ARCH detection (`runtime.GOOS`, `runtime.GOARCH`)
|
||||
- Thread-safe binary downloads with double-check locking
|
||||
- Health check polling with retries
|
||||
- Graceful shutdown with `sync.Once`
|
||||
- Free port allocation
|
||||
- Temp directory management
|
||||
- Version management via environment variables
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Mock Server with Generic DSL (Level 1)
|
||||
|
||||
**File**: `examples/jsonrpc-mock.md`
|
||||
|
||||
**Pattern**: Building generic mock servers with configurable responses using `AddMockResponse()`
|
||||
|
||||
**When to read:**
|
||||
- Need to mock ANY request/response protocol
|
||||
- Want readable test setup with DSL
|
||||
- Testing clients that call external APIs
|
||||
|
||||
**Applies to:**
|
||||
- **JSON-RPC** (shown in example) - RPC over HTTP
|
||||
- **REST APIs** - Use same pattern with route matching
|
||||
- **GraphQL** - Configure response per query
|
||||
- **gRPC** - Adapt for protobuf messages
|
||||
- **WebSocket** - Mock message responses
|
||||
- **Any HTTP-based protocol** - SOAP, XML-RPC, custom protocols
|
||||
|
||||
**Key techniques to adapt:**
|
||||
- Generic `AddMockResponse(identifier, response)` pattern
|
||||
- Using `httptest.Server` as foundation
|
||||
- Query/request tracking for assertions
|
||||
- Configuration-based response mapping
|
||||
- Thread-safe response storage
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: Bidirectional Streaming with Rich DSL (Level 1)
|
||||
|
||||
**File**: `examples/grpc-bufconn.md`
|
||||
|
||||
**Pattern**: In-memory bidirectional communication with rich client/server mocks
|
||||
|
||||
**When to read:**
|
||||
- Testing ANY bidirectional streaming protocol
|
||||
- Need full-duplex communication in tests
|
||||
- Want to avoid network I/O
|
||||
|
||||
**Applies to:**
|
||||
- **gRPC** (shown in example) - Uses bufconn for in-memory
|
||||
- **WebSockets** - Adapt bufconn pattern
|
||||
- **TCP streams** - Custom protocols over TCP
|
||||
- **Unix sockets** - Inter-process communication
|
||||
- **Any streaming protocol** - Server-Sent Events, HTTP/2 streams
|
||||
|
||||
**Key techniques to adapt:**
|
||||
- `bufconn` for in-memory connections (gRPC-specific, but concept applies)
|
||||
- Rich mock objects with helper methods
|
||||
- Thread-safe state tracking with mutexes
|
||||
- Assertion helpers (`ListenToStreamAndAssert()`)
|
||||
- When testing **server** → mock the **clients**
|
||||
- When testing **client** → mock the **server**
|
||||
|
||||
---
|
||||
|
||||
## Pattern 5: HTTP DSL and Builder Pattern (Level 1)
|
||||
|
||||
**File**: `examples/httptest-dsl.md`
|
||||
|
||||
**Pattern**: Building readable test infrastructure with DSL wrappers over stdlib
|
||||
|
||||
**When to read:**
|
||||
- Want to wrap ANY test infrastructure with clean DSL
|
||||
- Need fluent, readable test setup
|
||||
- Building reusable test utilities
|
||||
|
||||
**Applies to:**
|
||||
- **HTTP mocking** (shown in example) - httptest.Server wrapper
|
||||
- **Any test infrastructure** - Databases, queues, file systems
|
||||
- **Test data builders** - Fluent APIs for creating test data
|
||||
- **Custom test harnesses** - Wrapping complex setups
|
||||
|
||||
**Key techniques to adapt:**
|
||||
- Builder pattern with method chaining
|
||||
- Fluent API design (`OnGET().RespondJSON()`)
|
||||
- Separating configuration from execution
|
||||
- Type-safe builders with Go generics
|
||||
- Hiding complexity behind clean interfaces
|
||||
|
||||
---
|
||||
|
||||
## Pattern 6: Test Organization and Structure
|
||||
|
||||
**File**: `examples/test-organization.md`
|
||||
|
||||
**When to read:**
|
||||
- Setting up test structure for new projects
|
||||
- Adding build tags for integration tests
|
||||
- Configuring CI/CD for tests
|
||||
- Creating testutils package structure
|
||||
|
||||
**Universal patterns** (not technology-specific):
|
||||
- File organization (`pkg_test` package naming)
|
||||
- Build tags (`//go:build integration`)
|
||||
- Makefile/Taskfile structure
|
||||
- CI/CD configuration
|
||||
- testutils package layout
|
||||
|
||||
---
|
||||
|
||||
## Pattern 7: Integration Test Workflows
|
||||
|
||||
**File**: `examples/integration-patterns.md`
|
||||
|
||||
**When to read:**
|
||||
- Testing component interactions across package boundaries
|
||||
- Need patterns for Service + Repository testing
|
||||
- Testing workflows that span multiple components
|
||||
|
||||
**Universal patterns:**
|
||||
- Pattern 1: Service + Repository with in-memory deps
|
||||
- Pattern 2: Testing with real external services
|
||||
- Pattern 3: Multi-component workflow with testify suites
|
||||
- Dependency priority (in-memory > binary > test-containers)
|
||||
|
||||
---
|
||||
|
||||
## Pattern 8: System Test (Black Box)
|
||||
|
||||
**File**: `examples/system-patterns.md`
|
||||
|
||||
**When to read:**
|
||||
- Writing black-box end-to-end tests
|
||||
- Testing via CLI or API
|
||||
- Need tests that work without Docker
|
||||
|
||||
**Universal patterns:**
|
||||
- CLI testing with `exec.Command`
|
||||
- API testing with HTTP client
|
||||
- Dependency injection architecture
|
||||
- Pure Go testing (no Docker)
|
||||
|
||||
---
|
||||
|
||||
## How Claude Should Use These Files
|
||||
|
||||
### Pattern-Based Reading Rules
|
||||
|
||||
**When user needs to test with external dependencies:**
|
||||
|
||||
1. **Has official Go test harness?** → Read `nats-in-memory.md`
|
||||
- "Test with Redis/MongoDB/PostgreSQL/NATS"
|
||||
- "Avoid Docker but need real service"
|
||||
- Look for inspiration on wrapping official harnesses
|
||||
|
||||
2. **Need to download/run binary?** → Read `victoria-metrics.md`
|
||||
- "Test with Prometheus/Grafana/any binary"
|
||||
- "Manage binary dependencies"
|
||||
- Learn OS/ARCH detection, download patterns, health checks
|
||||
|
||||
3. **Need to mock request/response?** → Read `jsonrpc-mock.md`
|
||||
- "Mock REST/GraphQL/RPC/any HTTP API"
|
||||
- "Build mock with DSL"
|
||||
- Learn generic `AddMockResponse()` pattern
|
||||
|
||||
4. **Need bidirectional streaming?** → Read `grpc-bufconn.md`
|
||||
- "Test gRPC/WebSocket/streaming protocol"
|
||||
- "In-memory bidirectional communication"
|
||||
- Learn rich mock patterns, thread-safe state
|
||||
|
||||
5. **Want readable test DSL?** → Read `httptest-dsl.md`
|
||||
- "Build fluent test API"
|
||||
- "Wrap test infrastructure"
|
||||
- Learn builder pattern, method chaining
|
||||
|
||||
**When user asks about test structure:**
|
||||
- "How should I organize tests?" → Read `test-organization.md`
|
||||
- "How do I write integration tests?" → Read `integration-patterns.md`
|
||||
- "How do I write system tests?" → Read `system-patterns.md`
|
||||
|
||||
### Key Principle
|
||||
|
||||
**Examples show specific technologies (NATS, Victoria Metrics, JSON-RPC) but teach transferable patterns.**
|
||||
|
||||
Claude should:
|
||||
1. Identify the **pattern needed** (harness, binary, mock DSL, etc.)
|
||||
2. Read the **example file** that demonstrates that pattern
|
||||
3. **Adapt the techniques** to the user's specific technology
|
||||
4. Use the example as a **template**, not a literal solution
|
||||
|
||||
### Default Behavior (No Example Needed)
|
||||
|
||||
For simple scenarios, use the core patterns in this file:
|
||||
- Basic table-driven tests → Use patterns from this file
|
||||
- Simple testify suites → Use patterns from this file
|
||||
- Basic synchronization → Use patterns from this file
|
||||
- Simple in-memory implementations → Use InMemoryRepository/TestEmailer from this file
|
||||
|
||||
**Read example files when patterns/techniques are needed, not just for specific tech.**
|
||||
|
||||
---
|
||||
|
||||
## Final Notes
|
||||
|
||||
This reference provides core testing principles and patterns. For detailed implementations and complete examples, refer to the example files listed above. Each example file is self-contained and can be read independently based on your testing needs.
|
||||
Reference in New Issue
Block a user