6.0 KiB
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_testpackage name
Integration Tests
integration_test.goor*_integration_test.go- Use
//go:build integrationtag - Can be in same directory or separate
integration/folder - Use
pkg_testpackage name
System Tests
*_test.gointests/directory at project root- No build tags needed (separate directory)
- Use
testsormain_testpackage 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
- Co-locate unit tests - Next to the code being tested
- Use pkg_test package - Forces public API testing
- Build tags for integration - Keep unit tests fast by default
- Separate system tests - In
tests/directory - Test your test infrastructure - Treat testutils as production code
- Reusable infrastructure - Share across all test levels