Testing standards
Test Organization
Section titled “Test Organization”Directory structure
Section titled “Directory structure”Tests live in three places, matching the three test tiers:
| Test Type | Location | Description |
|---|---|---|
| Unit | modules/{module}/*_test.go (ce) / modules/{module}/*_test.go | Fast, isolated tests against fakes |
| Integration | tests/integration/... + tests/parity/... | Tests that touch the DB, daemon, or external services |
| E2E | tests/e2e/*.bats | End-to-end bats suites against a real daemon |
Module test structure
Section titled “Module test structure”Each Go module keeps its tests next to the source:
modules/{module}/ service.go service_test.go router.go router_test.go errors.go errors_test.go types.go types_test.goFor submodules (e.g. providers) the same pattern repeats:
modules/vault/ service.go service_test.go providers/ local/ local.go local_test.go aws/ aws.go aws_test.goTest frameworks
Section titled “Test frameworks”| Type | Tool | Location | Runner |
|---|---|---|---|
| Unit | testing stdlib | *_test.go beside source + tests/unit/ | make test/unit |
| App | Bats | apps/*/tests/*.bats | make test/apps |
| E2E | Bats | tests/e2e/*.bats | make test/e2e |
| Infra | Bats | tests/proxmox/*.bats | make test/proxmox |
Integration tests live under tests/integration/... and tests/parity/...
and use the same testing stdlib; they’re separated only by make
target so the unit pass stays fast.
Running tests
Section titled “Running tests”# Fast unit pass — source tree + tests/unit/make test/unit
# Integration + paritymake test/integration
# Everything (unit + integration)make test
# Run a specific package (in the ce checkout for ce packages)go test ./modules/backup/...
# Run a single test by name (in the ce checkout for ce packages)go test -run TestStackRepoUpsertInsertAssignsID ./modules/app/
# Race detector (the integration lane enables it when the toolchain supports it)make test/integration
# Coverage report (HTML; make test/unit writes coverage.out itself)go test -coverprofile=coverage.out ./... ./cmd/...go tool cover -html=coverage.out -o coverage.htmlUnit test structure
Section titled “Unit test structure”// modules/backup/lease_test.go (example)package backup_test
import ( "context" "testing"
"gitlab.com/bitspur/selfhosted-cloud/ce/modules/backup")
func TestService_CreateBackup_Success(t *testing.T) { svc := newTestService(t) res, err := svc.CreateBackup(t.Context(), backup.BackupCreate{ StackName: "mystack", Paths: []string{"/var/lib/mystack"}, }) if err != nil { t.Fatalf("CreateBackup: %v", err) } if res.BackupID == "" { t.Errorf("BackupID empty") }}
func TestService_CreateBackup_StackNotFound(t *testing.T) { svc := newTestService(t) _, err := svc.CreateBackup(t.Context(), backup.BackupCreate{ StackName: "nonexistent", Paths: []string{"/x"}, }) var ce *coded.CodedError if !errors.As(err, &ce) || ce.Code != "X404401STKNOF" { t.Errorf("error code = %v, want X404401STKNOF", err) }}Test naming
Section titled “Test naming”// Pattern: Test{Type}_{Method}_{Scenario}func TestService_CreateBackup_Success(t *testing.T) { /* … */ }func TestService_CreateBackup_StackNotFound(t *testing.T) { /* … */ }func TestService_CreateBackup_DiskFull(t *testing.T) { /* … */ }Fakes vs mocks
Section titled “Fakes vs mocks”The project prefers hand-rolled fakes over generated mocks. A fake
implements the same interface the production code consumes (e.g.
backup.RecoveryActions, vault.SecretRenamer) and records the
arguments it was called with for assertions. Generated mocks add a
dependency on a mocking framework that no one wants to debug.
type fakeNodeRepo struct { upserts []cluster.ClusterNodeResponse}
func (f *fakeNodeRepo) Upsert(_ context.Context, n cluster.ClusterNodeResponse) error { f.upserts = append(f.upserts, n) return nil}E2E tests (Bats)
Section titled “E2E tests (Bats)”#!/usr/bin/env bats
setup() { # Setup before each test shc install postgres --stack test-pg}
teardown() { # Cleanup after each test shc uninstall test-pg --force}
@test "backup creates archive file" { run shc backup test-pg [ "$status" -eq 0 ]
# Check backup file exists [ -f "$HOME/.local/share/shc/backups/"*.bak ]}
@test "restore from backup succeeds" { shc backup test-pg shc uninstall test-pg --force
run shc restore test-pg --latest [ "$status" -eq 0 ]}What to test
Section titled “What to test”DO test
Section titled “DO test”- Core user flows (install, backup, restore)
- Business logic in services
- Error conditions for critical paths
- Edge cases that have caused bugs
DON’T test
Section titled “DON’T test”- Trivial getters / setters
- Framework code (chi router, rqlited)
- Every possible input combination
- Implementation details
Coverage
Section titled “Coverage”Aim for coverage on critical paths, not 100%:
- Unit tests for service layer business logic
- Unit tests for error handling paths
- Unit tests for CLI command flows
- Skip trivial code
- Skip generated code
See Also
Section titled “See Also”shc@docs:~$