Files
gh-buzzdan-ai-coding-rules-…/skills/testing/examples/integration-patterns.md
2025-11-29 18:02:42 +08:00

6.0 KiB

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: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: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: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: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

# 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