Writing tests
See Testing Standards for the full test-tier table, framework matrix, and authoring conventions. This page captures the practical “how do I add a test today” recipes.
Where does the test live?
Section titled “Where does the test live?”| Kind | Path |
|---|---|
| Pure-Go unit (no DB, no daemon) | modules/{mod}/foo_test.go (ce) / modules/{mod}/foo_test.go |
| Touches DB / daemon | tests/integration/... |
| Parity check vs manifest | tests/parity/... |
| App lifecycle (compose up/down) | apps/<app>/tests/tests.bats |
| End-to-end against running daemon | tests/e2e/*.bats |
| Multi-node infra | tests/proxmox/*.bats |
Running tests
Section titled “Running tests”# Fast unit pass — source tree + tests/unit/make test/unit
# Add coverage HTMLgo test -coverprofile=coverage.out ./internal/...go tool cover -html=coverage.out -o coverage.html
# Specific packagego test ./internal/modules/backup/...
# Single test by namego test -run TestService_CreateBackup_Success ./internal/modules/backup/Unit test template
Section titled “Unit test template”// modules/backup/lease_test.go (example)package backup_test
import ( "errors" "testing"
"gitlab.com/bitspur/selfhosted-cloud/ce/core/coded" "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”Test{Type}_{Method}_{Scenario}Examples: TestService_CreateBackup_Success,
TestRouter_PostInstall_StackNotFound,
TestStackRepo_Upsert_OnConflictUpdates.
Fakes, not mocks
Section titled “Fakes, not mocks”Hand-roll a fake that implements the interface the production code consumes; record arguments for assertions. Avoid generated mock frameworks — they hide more than they save.
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() { shc install postgres --stack test-pg}
teardown() { shc uninstall test-pg --force}
@test "backup creates archive file" { run shc backup test-pg [ "$status" -eq 0 ] [ -f "$HOME/.local/share/shc/backups/"*.bak ]}What to test
Section titled “What to test”DO test: core user flows (install, backup, restore), service-layer business logic, error conditions for critical paths, edge cases that have caused bugs.
DON’T test: trivial getters/setters, framework code (chi router, gorqlite, cobra), every possible input combination, implementation details.