Skip to content
SHC Docs

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.

KindPath
Pure-Go unit (no DB, no daemon)modules/{mod}/foo_test.go (ce) / modules/{mod}/foo_test.go
Touches DB / daemontests/integration/...
Parity check vs manifesttests/parity/...
App lifecycle (compose up/down)apps/<app>/tests/tests.bats
End-to-end against running daemontests/e2e/*.bats
Multi-node infratests/proxmox/*.bats
Terminal window
# Fast unit pass — source tree + tests/unit/
make test/unit
# Add coverage HTML
go test -coverprofile=coverage.out ./internal/...
go tool cover -html=coverage.out -o coverage.html
# Specific package
go test ./internal/modules/backup/...
# Single test by name
go test -run TestService_CreateBackup_Success ./internal/modules/backup/
// 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{Type}_{Method}_{Scenario}

Examples: TestService_CreateBackup_Success, TestRouter_PostInstall_StackNotFound, TestStackRepo_Upsert_OnConflictUpdates.

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
}
tests/e2e/backup.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 ]
}

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.