Initial commit
This commit is contained in:
248
skills/testing/examples/integration-patterns.md
Normal file
248
skills/testing/examples/integration-patterns.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# Integration Test Patterns
|
||||
|
||||
## Purpose
|
||||
|
||||
Integration tests verify that components work together correctly. They test the seams between packages, ensure proper data flow, and validate that integrated components behave as expected.
|
||||
|
||||
**When to Write**: After unit testing individual components, test how they interact.
|
||||
|
||||
## File Organization
|
||||
|
||||
### Option 1: In Package with Build Tags (Preferred)
|
||||
|
||||
```go
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"myproject/internal/testutils"
|
||||
)
|
||||
|
||||
func TestUserService_Integration(t *testing.T) {
|
||||
// Integration test
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Separate Package
|
||||
|
||||
```
|
||||
user/
|
||||
├── user.go
|
||||
├── user_test.go # Unit tests
|
||||
└── integration/
|
||||
└── user_integration_test.go # Integration tests
|
||||
```
|
||||
|
||||
## Pattern 1: Service + Repository (In-Memory)
|
||||
|
||||
**Use when**: Testing service logic with data persistence
|
||||
|
||||
```go
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"myproject/user"
|
||||
)
|
||||
|
||||
func TestUserService_CreateAndRetrieve(t *testing.T) {
|
||||
// Setup: In-memory repository (Level 1)
|
||||
repo := user.NewInMemoryRepository()
|
||||
svc := user.NewUserService(repo, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create user
|
||||
userID, _ := user.NewUserID("usr_123")
|
||||
email, _ := user.NewEmail("alice@example.com")
|
||||
newUser := user.User{
|
||||
ID: userID,
|
||||
Name: "Alice",
|
||||
Email: email,
|
||||
}
|
||||
|
||||
err := svc.CreateUser(ctx, newUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Retrieve user
|
||||
retrieved, err := svc.GetUser(ctx, userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Alice", retrieved.Name)
|
||||
require.Equal(t, email, retrieved.Email)
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 2: Testing with Real External Service
|
||||
|
||||
**Use when**: Need to test against real service behavior (Victoria Metrics, NATS, etc.)
|
||||
|
||||
```go
|
||||
//go:build integration
|
||||
|
||||
package metrics_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"myproject/internal/testutils"
|
||||
"myproject/metrics"
|
||||
)
|
||||
|
||||
func TestMetricsIngest_WithVictoriaMetrics(t *testing.T) {
|
||||
// Start real Victoria Metrics (Level 2 - binary)
|
||||
vmServer, err := testutils.RunVictoriaMetricsServer()
|
||||
require.NoError(t, err)
|
||||
defer vmServer.Shutdown()
|
||||
|
||||
// Create service with real dependency
|
||||
svc := metrics.NewIngester(vmServer.WriteURL())
|
||||
|
||||
// Test ingestion
|
||||
err = svc.IngestMetric(context.Background(), "test_metric", 42.0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Force flush and verify
|
||||
vmServer.ForceFlush(context.Background())
|
||||
results, err := testutils.QueryVictoriaMetrics(vmServer.QueryURL(), "test_metric")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 3: Multi-Component Workflow
|
||||
|
||||
**Use when**: Testing complete workflows across multiple components
|
||||
|
||||
```go
|
||||
//go:build integration
|
||||
|
||||
package workflow_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"myproject/internal/testutils"
|
||||
"myproject/user"
|
||||
"myproject/notification"
|
||||
)
|
||||
|
||||
type UserWorkflowSuite struct {
|
||||
suite.Suite
|
||||
userRepo *user.InMemoryRepository
|
||||
emailer *user.TestEmailer
|
||||
natsServer *nserver.Server
|
||||
userService *user.UserService
|
||||
notifSvc *notification.NotificationService
|
||||
}
|
||||
|
||||
func (s *UserWorkflowSuite) SetupSuite() {
|
||||
// Setup in-memory NATS (Level 1)
|
||||
natsServer, err := testutils.RunNATsServer()
|
||||
s.Require().NoError(err)
|
||||
s.natsServer = natsServer
|
||||
|
||||
// Setup components
|
||||
s.userRepo = user.NewInMemoryRepository()
|
||||
s.emailer = user.NewTestEmailer()
|
||||
s.userService = user.NewUserService(s.userRepo, s.emailer)
|
||||
|
||||
natsAddr := "nats://" + natsServer.Addr().String()
|
||||
s.notifSvc = notification.NewService(natsAddr)
|
||||
}
|
||||
|
||||
func (s *UserWorkflowSuite) TearDownSuite() {
|
||||
s.natsServer.Shutdown()
|
||||
}
|
||||
|
||||
func (s *UserWorkflowSuite) TestCreateUser_TriggersNotification() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Subscribe to notifications
|
||||
received := make(chan string, 1)
|
||||
s.notifSvc.Subscribe("user.created", func(msg string) {
|
||||
received <- msg
|
||||
})
|
||||
|
||||
// Create user
|
||||
userID, _ := user.NewUserID("usr_123")
|
||||
email, _ := user.NewEmail("alice@example.com")
|
||||
newUser := user.User{ID: userID, Name: "Alice", Email: email}
|
||||
|
||||
err := s.userService.CreateUser(ctx, newUser)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Verify notification sent
|
||||
select {
|
||||
case msg := <-received:
|
||||
s.Contains(msg, "Alice")
|
||||
case <-time.After(2 * time.Second):
|
||||
s.Fail("timeout waiting for notification")
|
||||
}
|
||||
|
||||
// Verify email sent
|
||||
emails := s.emailer.SentEmails()
|
||||
s.Contains(emails, "alice@example.com")
|
||||
}
|
||||
|
||||
func TestUserWorkflowSuite(t *testing.T) {
|
||||
suite.Run(t, new(UserWorkflowSuite))
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Priority
|
||||
|
||||
1. **Level 1: In-Memory** (Preferred) - httptest, in-memory maps, NATS harness
|
||||
2. **Level 2: Binary** (When needed) - Victoria Metrics, standalone services
|
||||
3. **Level 3: Test-containers** (Last resort) - Docker containers, slow startup
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO:
|
||||
- Test seams between components
|
||||
- Use in-memory implementations when possible
|
||||
- Test happy path and error scenarios
|
||||
- Use testify suites for complex setup
|
||||
- Focus on data flow and integration points
|
||||
|
||||
### DON'T:
|
||||
- Don't test business logic (that's unit tests)
|
||||
- Don't use heavy mocking (use real implementations)
|
||||
- Don't require Docker unless absolutely necessary
|
||||
- Don't duplicate unit test coverage
|
||||
- Don't skip cleanup (always defer)
|
||||
|
||||
## Running Integration Tests
|
||||
|
||||
```bash
|
||||
# Skip integration tests (default)
|
||||
go test ./...
|
||||
|
||||
# Run with integration tests
|
||||
go test -tags=integration ./...
|
||||
|
||||
# Run only integration tests
|
||||
go test -tags=integration ./... -run Integration
|
||||
|
||||
# With coverage
|
||||
go test -tags=integration -coverprofile=coverage.out ./...
|
||||
```
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Test component interactions** - Not individual units
|
||||
2. **Prefer real implementations** - Over mocks when possible
|
||||
3. **Use build tags** - Keep unit tests fast
|
||||
4. **Reuse testutils** - Same infrastructure across tests
|
||||
5. **Test workflows** - Not just individual operations
|
||||
Reference in New Issue
Block a user