Skip to content
SHC Docs

Testing standards

Tests live in three places, matching the three test tiers:

Test TypeLocationDescription
Unitmodules/{module}/*_test.go (ce) / modules/{module}/*_test.goFast, isolated tests against fakes
Integrationtests/integration/... + tests/parity/...Tests that touch the DB, daemon, or external services
E2Etests/e2e/*.batsEnd-to-end bats suites against a real daemon

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.go

For 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.go
TypeToolLocationRunner
Unittesting stdlib*_test.go beside source + tests/unit/make test/unit
AppBatsapps/*/tests/*.batsmake test/apps
E2EBatstests/e2e/*.batsmake test/e2e
InfraBatstests/proxmox/*.batsmake 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.

Terminal window
# Fast unit pass — source tree + tests/unit/
make test/unit
# Integration + parity
make 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.html
// 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)
}
}
// 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) { /* … */ }

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
}
tests/e2e/backup.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 ]
}
  • Core user flows (install, backup, restore)
  • Business logic in services
  • Error conditions for critical paths
  • Edge cases that have caused bugs
  • Trivial getters / setters
  • Framework code (chi router, rqlited)
  • Every possible input combination
  • Implementation details

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