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

6.0 KiB

Test Organization and File Structure

File Organization

Basic Structure

user/
├── user.go
├── user_test.go          # Unit tests for user.go
├── service.go
├── service_test.go       # Unit tests for service.go
├── repository.go
└── repository_test.go    # Unit tests for repository.go

With Integration and System Tests

project/
├── user/
│   ├── user.go
│   ├── user_test.go              # Unit tests (pkg_test)
│   ├── service.go
│   ├── service_test.go           # Unit tests (pkg_test)
│   └── integration_test.go       # Integration tests with //go:build integration
├── internal/
│   └── testutils/                # Reusable test infrastructure
│       ├── nats.go              # In-memory NATS server
│       ├── victoria.go          # Victoria Metrics binary management
│       └── httpserver/          # HTTP mock DSL
│           ├── server.go
│           └── server_test.go   # Test the infrastructure!
└── tests/                        # System tests (black box)
    ├── cli_test.go              # CLI testing via exec.Command
    └── api_test.go              # API testing via HTTP client

Package Naming

Use pkg_test for Unit Tests

// ✅ External package - tests public API only
package user_test

import (
    "testing"
    "github.com/yourorg/project/user"
)

func TestService_CreateUser(t *testing.T) {
    // Test through public API
    svc, _ := user.NewUserService(repo, notifier)
    err := svc.CreateUser(ctx, testUser)
    // ...
}

Avoid Same Package Testing

// ❌ Same package - can test private methods (don't do this)
package user

import "testing"

func TestInternalValidation(t *testing.T) {
    // Testing private function - bad practice
    result := validateEmailInternal("test@example.com")
    // ...
}

Build Tags for Integration Tests

Using Build Tags

//go:build integration

package user_test

import (
    "context"
    "testing"
    "myproject/internal/testutils"
)

func TestUserService_Integration(t *testing.T) {
    // Integration test with real dependencies
    natsServer, _ := testutils.RunNATsServer()
    defer natsServer.Shutdown()

    // Test with real NATS
    // ...
}

Running Tests

# Run only unit tests (default - no build tags)
go test ./...

# Run unit + integration tests
go test -tags=integration ./...

# Run specific package integration tests
go test -tags=integration ./user

# Run system tests only
go test ./tests/...

# Run all tests
go test -tags=integration ./...

Makefile/Taskfile Integration

Taskfile.yml Example

version: '3'

tasks:
  test:
    desc: Run unit tests
    cmds:
      - go test -v -race ./...

  test:integration:
    desc: Run integration tests
    cmds:
      - go test -v -race -tags=integration ./...

  test:system:
    desc: Run system tests
    cmds:
      - go test -v -race ./tests/...

  test:all:
    desc: Run all tests
    cmds:
      - task: test:integration
      - task: test:system

  test:coverage:
    desc: Run tests with coverage
    cmds:
      - go test -v -race -coverprofile=coverage.out ./...
      - go tool cover -html=coverage.out -o coverage.html

Makefile Example

.PHONY: test test-integration test-system test-all coverage

test:
	go test -v -race ./...

test-integration:
	go test -v -race -tags=integration ./...

test-system:
	go test -v -race ./tests/...

test-all: test-integration test-system

coverage:
	go test -v -race -coverprofile=coverage.out ./...
	go tool cover -html=coverage.out -o coverage.html

Test File Naming

Unit Tests

  • *_test.go - Standard test files
  • Located next to the code being tested
  • Use pkg_test package name

Integration Tests

  • integration_test.go or *_integration_test.go
  • Use //go:build integration tag
  • Can be in same directory or separate integration/ folder
  • Use pkg_test package name

System Tests

  • *_test.go in tests/ directory at project root
  • No build tags needed (separate directory)
  • Use tests or main_test package name

CI/CD Integration

GitHub Actions Example

name: Tests

on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      - name: Run unit tests
        run: go test -v -race ./...

  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      - name: Run integration tests
        run: go test -v -race -tags=integration ./...

  system-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      - name: Build application
        run: go build -o myapp ./cmd/myapp
      - name: Run system tests
        run: go test -v -race ./tests/...

testutils Package Structure

internal/testutils/
├── nats.go              # NATS in-memory server helpers
├── victoria.go          # Victoria Metrics binary management
├── prometheus.go        # Prometheus payload helpers
├── grpc_client_mock.go  # gRPC client mock with DSL
├── jrpc_server_mock.go  # JSON-RPC server mock with DSL
└── httpserver/          # HTTP mock server with DSL
    ├── server.go
    ├── server_test.go   # Test the infrastructure!
    ├── dsl.go
    └── README.md

Key Principles

  1. Co-locate unit tests - Next to the code being tested
  2. Use pkg_test package - Forces public API testing
  3. Build tags for integration - Keep unit tests fast by default
  4. Separate system tests - In tests/ directory
  5. Test your test infrastructure - Treat testutils as production code
  6. Reusable infrastructure - Share across all test levels