restic — Testing#
Test metrics#
- Test files: 211
- Source files (non-test): 325
- Ratio (test / source): ~0.65 — notably high for a systems-level tool
- Test frameworks: stdlib
testingonly — no testify, gomock, ginkgo, or any third-party assertion library
Test organization#
Placement#
Mixed strategy, deliberately chosen per test type:
_testpackage (black-box): most backend tests, filter tests, integration tests incmd/restic- Same package (white-box): internal tests that need access to unexported fields, e.g.,
local_internal_test.go(packagelocal) alongsidelocal_test.go(packagelocal_test) in the same directory. Both styles coexist in the same package directory when needed.
Helper packages#
internal/test— project-wide test utility package. Contains:Assert,OK,OKs— simple condition/error assertion helpers styled like testify but handrolledEquals[T any]— generic equality check usingreflect.DeepEqualRandom(seed, count int) []byte— deterministic pseudo-random data generator (seeded, reproducible)TempDir(t)— creates a temp directory registered witht.CleanupSetupTarTestFixture— extracts a.tar.gzor.bzip2archive fixture into a temp dirEnv(t, repoFixture)— creates a full test repo environment from a tar fixture; returns path + cleanup closureChdir(t, dest)— changes working directory, returns a restore funcRemoveAll,ResetReadOnly— Windows-safe recursive deletion helpersvars.go— all test knobs are env-var-controlled:RESTIC_TEST_PASSWORD,RESTIC_TEST_CLEANUP,RESTIC_TEST_TMPDIR,RESTIC_TEST_INTEGRATION,RESTIC_TEST_FUSE,RESTIC_TEST_S3_SERVER,RESTIC_TEST_REST_SERVER,RESTIC_TEST_DISALLOW_SKIP
internal/backend/mock— hand-rolled mock forbackend.Backend. Uses function-field structs: every method is aFnField func(...)that defaults to a no-op orerrors.New("not implemented")if nil. Callers override only the methods they need:be.SaveFn = func(...) error { ... }. A compile-time assertionvar _ backend.Backend = &Backend{}guards correctness.internal/backend/test— a generic backend acceptance test suite.Suite[C any]is a generic struct parameterized on the backend config type. It discovers and runs all methods namedTest*/Benchmark*via reflection (reflect.TypeOf(s).NumMethod()). Every official backend (local, sftp, s3, rest, rclone, cache, sema) callssuite.RunTests(t)in a single line, getting full CRUD/concurrency/edge-case coverage for free.internal/repository/testing.go— production-package test helpers (not a_testfile, so they can be imported by other packages):TestRepository(t)— returns a fully initialised in-memory repository, fast and hermeticTestRepositoryWithBackend(t, be, version, opts)— parameterised versionTestUseLowSecurityKDFParameters(t)— replaces scrypt params withN=128, R=1, P=1usingsync.Once, making KDF cost negligible without per-test boilerplateTestAllVersions(t, fn)/BenchmarkAllVersions(b, fn)— parametrize over all supported repo format versionsTestFromFixture(t, repoFixture)— opens a repo from a tar archive fixtureTestCheckRepo(t, repo)— runs the full integrity checker on a test repo
internal/backend/retry/testing.go—TestFastRetries()sets global retry delays to 1 ms, preventing slow exponential backoffs in tests.internal/restic/config.go—TestDisableCheckPolynomial(t)disables the CDC polynomial validation, which is expensive and unnecessary for most tests.
Fixtures#
cmd/restic/testdata/— containsbackup-data.tar.gz: a real directory tree used as backup source in integration tests.testSetupBackupDataextracts it at test time.internal/backend/testdata/,internal/checker/testdata/,internal/filter/testdata/,internal/repository/testdata/— per-package fixture repositories and data files.- Fixtures are tar archives extracted at test start; no Docker or external daemon required for the majority of tests.
Test patterns#
Table-driven tests#
- Prevalence: Heavy. 228 usages of
t.Run/tt.Run/testCasespatterns in test files. - Style: Anonymous struct slices are the dominant form:(
var matchTests = []struct { pattern string path string match bool }{ {"*.go", "/foo/bar/test.go", true}, {"*.c", "/foo/bar/test.go", false}, ... }internal/filter/filter_test.gohas 100+ rows in a single table.) - Subtests:
t.Run(name, fn)used consistently when a name is available; some older tests iterate without subtests. - Example:
internal/filter/filter_test.go:14— largematchTestsslice;internal/restic/parallel_test.go— concurrent operation tables.
Mocking approach#
- Strategy: Hand-rolled function-field mocks, not generated. No
gomock, nomockery. - Pattern: The
mock.Backendstruct has one exportedFnFieldper interface method. Ifnil, the method either no-ops or returnserrors.New("not implemented"). Tests set only the fields they care about. This is lighter-weight than gomock and avoids code generation in the build. - Example:
internal/backend/mock/backend.go—SaveFn func(...),ListFn func(...), etc. Used heavily in backend decorator tests (retry, sema, cache layers).
Integration tests#
- Present: Yes, an extensive suite.
- Separation: By naming convention —
cmd_*_integration_test.goincmd/restic/. There are 20 integration test files, one per major command (backup, restore, check, forget, prune, copy, diff, rewrite, find, ls, list, tag, key, mount, snapshots, generate, init, recover, repair-index, repair-snapshots). - How: All-in-process — no Docker, no external services for core integration tests. The
withTestEnvironmenthelper creates a temporary directory tree with a live local-backend repository. It applies three critical speed knobs:TestUseLowSecurityKDFParameters,TestDisableCheckPolynomial,TestFastRetries. Tests call the samerunBackup,runRestore,runCheck, etc. functions as the real CLI, with a capturedtermstatusoutput. - External backend tests (optional): S3 (Minio), REST server, SFTP — enabled by env vars (
RESTIC_TEST_S3_SERVER,RESTIC_TEST_REST_SERVER). The CI workflow (tests.yml) starts Minio and a rest-server on Linux runners to exercise these paths. ARESTIC_TEST_DISALLOW_SKIPenv var can force specific tests to fail rather than skip, ensuring CI doesn’t silently drop coverage. - Race detector: The CI matrix includes a dedicated
Linux (race)job runninggo test -race. The errgroup + channel concurrency patterns are specifically written to be race-free. - Backdoor hook:
global.Options.BackendTestHook func(backend.Backend) (backend.Backend, error)— allows tests to wrap the backend with inspection or fault injection without changing production code. Used in integration tests to enforce that each file type is listed at most once (newOrderedListOnceBackend).
Backend acceptance suite pattern#
internal/backend/test.Suite[C]uses reflection to discoverTest*methods on the suite struct. Each backend callssuite.RunTests(t)once; the suite creates a fresh backend, runs 15+ behavioral tests as subtests, then cleans up. This is the closest analogue to a parameterized test suite available without third-party frameworks.- Multi-version parametrization:
TestAllVersions(t, fn)iterates overMinRepoVersion..MaxRepoVersion, calling the test function for each. This ensures every supported repository format is exercised without duplicating test code.
Test quality observations#
What’s done well#
- Zero third-party test dependencies: The entire test suite uses only stdlib
testing. The custominternal/testhelpers are purpose-built and minimal. This keepsgo test ./...fast and dependency-free. - In-process integration tests: Running real command implementations against a real (local-disk) repository gives high confidence without Docker overhead. Tests cover round-trips: backup → restore → diff → check.
- Speed knobs without test-only binaries:
TestUseLowSecurityKDFParameters,TestFastRetries,TestDisableCheckPolynomialare production-package functions (not in_test.go) that can be called by any package’s tests. This avoids copying KDF setup boilerplate across 20 integration tests. - Generic backend suite:
Suite[C any]with reflection-based discovery is a clean solution to the “run the same test against N implementations” problem without inheritance or code generation. - Environment-variable configurability: All external test dependencies (S3, REST, SFTP) can be injected via env vars. Tests skip gracefully when the service is absent, but CI can enforce full runs with
RESTIC_TEST_DISALLOW_SKIP. - Race detector in CI: Dedicated
-racematrix job gives confidence that the errgroup/channel concurrency patterns are correct. - Fixture archives: Using
.tar.gzfixtures for both test repositories and backup source data provides stable, reproducible test inputs.
What could improve#
- No test names in some tables: Several older table-driven tests iterate without subtests, making failure messages like
FAIL: index 23hard to debug. - Reflection-based suite discovery: The
Suite.testFuncsreflection loop is fragile — method signature mismatches produce only a log warning, not a compile error. Ago generate-based approach or explicit registration would be more robust. - Test helpers in production packages: Files like
internal/repository/testing.goandinternal/backend/retry/testing.goship in the non-test binary (testingpackage imported in non-_test.gofiles). This is deliberate but adds atestingimport to the production binary. Restic accepts this trade-off for usability. - No fuzz tests: The CDC chunker, crypto layer, and backend parsers would benefit from
go test -fuzzharnesses; none are present.
Patterns worth emulating#
- The
internal/testpackage approach: a small set of handrolled assertion helpers (Assert, OK, Equals[T]) is all that’s needed and avoids testify as a dependency. - The function-field mock pattern (
mock.Backend) for mocking an interface: zero codegen, easy to extend, and thenil-check default behavior reduces test setup boilerplate dramatically. - The test knob in production code pattern:
TestUseLowSecurityKDFParameterslives ininternal/repository(not_test.go) so it can be called from anywhere. For libraries with expensive-but-adjustable parameters (crypto, networking), this is a powerful technique. - The generic acceptance suite (
Suite[C any]): demonstrates that a single well-designed test suite can verify all implementations of an interface without per-implementation duplication, using Go generics + reflection. - The
BackendTestHookinglobal.Options: allowing tests to wrap the backend with a hook function at the composition root is a clean fault-injection mechanism that doesn’t pollute production types with test conditionals.