Harness Open Source (Drone/Gitness) — Testing#
Test metrics#
- Test files: 212
*_test.gofiles (excluding vendor) - Total Go files: 2,578
- Ratio (test files / source files): ~1:12 (~8%) — sparse for a project of this size
- Unique packages with tests: 98
- Table-driven test occurrences: 577 (
testCases,t.Run,tt.Runpatterns) - Test frameworks:
testify/assert,testify/require(primary);testify/mock(mocking); mockery v2.53.3 (code generation); stdlibtesting(some packages)
Test organization#
- Placement: Mixed — some tests are in the same package (white-box, e.g.,
audit/context_test.goin packageaudit), others use the external_testpackage (black-box, e.g.,registry/app/api/handler/cargo/*_test.goinpackage cargo_test). No consistent project-wide policy. - Helper packages:
app/testing/— present as a directory withtesting.goandintegration/integration.gobut both files contain only the package declaration; effectively empty stubs for future use.registry/app/api/controller/mocks/— mockery-generated mocks for the registry controller interfaces (e.g.,Controller,ArtifactRepository,Authorizer, ~30 files). Generated withmockery v2.53.3, each file carries// Code generated by mockery.
- Fixtures:
registry/app/api/controller/metadata/testdata/— JSON fixture files for registry metadata controller tests. No othertestdatadirectories found in the main application.
Test patterns#
Table-driven tests#
- Prevalence: Heavy — 577 occurrences across the codebase. Dominant pattern wherever tests exist.
- Style: Anonymous struct slice with a
namefield, iterated witht.Run(test.name, ...). - Example:
store/database/dbtx/runner_test.go:31—tests := []struct{ name string; fn func(...); errCommit error; ... }{}with 10 cases covering commit, rollback, panic, and context-cancellation scenarios inTestWithTx. - Variant: Some test functions use subtests without a slice — just sequential
t.Run(...)blocks for labeling (e.g.,audit/context_test.go—t.Run("returns IP when present", ...)directly). This avoids struct overhead for simple property-based checks.
Mocking approach#
Two distinct strategies depending on subsystem:
1. Mockery-generated mocks (registry package only):
- Located in
registry/app/api/controller/mocks/. - Generated from
testify/mock-based structs. Each mock embedsmock.Mockand implements the interface with_m.Called(...)delegation. - Test files import these mocks and use
On(...).Return(...)+AssertExpectations(t). - Example:
registry/app/api/handler/cargo/upload_test.go:67—mockCtrl.On("UploadPackage", ctx, info, mock.AnythingOfType(...), mock.Anything).Return(resp, nil).
2. Manual functional mocks (core application):
- Hand-written structs with function-field callbacks:
struct { FindByRefFn func(ctx context.Context, ref string) (*types.SpaceCore, error) }. - Seen in
app/services/usage/middleware_int_test.go:168—SpaceFinderMockandMetricsMockuse this pattern. The struct method delegates to the function field. - Also seen in
store/database/dbtx/runner_test.go:168—dbMockandtxMockare hand-written interface implementations that track call state (.committed,.rollbackbooleans) without any mock framework.
3. Interface satisfaction assertion:
var _ transactor = (*dbMock)(nil)compile-time interface check (runner_test.go:176). Common in the codebase for verifying mock correctness at compile time.
- Located in
Integration tests#
- Present: Yes, but extremely limited — only one file found:
app/services/usage/middleware_int_test.go. - Separation:
//go:build integrationbuild tag (plus the older// +build integrationdual annotation for pre-1.17 compatibility). - How: The integration test (
TestUploadDownloadMiddleware) spins up a realchiHTTP server in a goroutine, then fires concurrent HTTP requests againstlocalhost:8080. It uses awaitServer()helper with retry logic to wait for the server to be ready. - External dependencies: Expects a running HTTP listener — not testcontainers, not dockertest. Database integration tests are absent; the database layer is tested with in-memory mocks (see
dbMockabove). - No
*_e2e_test.gofiles found anywhere in the repository.
Test quality observations#
What’s done well#
- Table-driven tests are thorough where they exist. The
dbtx/runner_test.gocovers 10 edge cases for transaction semantics (panic recovery, commit-after-cancel, double-finish), all within one table. The URL backfill tests incli/operations/server/config_test.gocover 17 scenarios exhaustively. - Interface satisfaction at compile time.
var _ Interface = (*Mock)(nil)is used systematically in test files to catch drift between the mock and its interface. - Build tag separation. The single integration test file cleanly separates itself from unit tests using
//go:build integration, sogo test ./...never requires a live server. - HTTP handler tests use
net/http/httptest. 18 files test HTTP handlers without a live server. The cargo upload test exercises success, bad payload, controller error, and JSON encoding failure paths — a thorough handler test. - Dual in-memory/Redis broker makes stream tests possible without Redis. The
stream/MemoryBrokeris used in unit tests while production uses Redis; the common interface makes this transparent (noted in patterns analysis). testify/requirefor fast-fail. Tests userequire.NoError/require.Equalat the start to abort immediately on setup failure, avoiding cascading assertion noise.
What could improve#
- Test coverage is sparse for a large codebase (8%). Critical subsystems —
git/,app/pipeline/,app/router/,events/,store/database/(main store) — have few or no tests. The Wire-generated DI chain (cmd/gitness/wire_gen.go) is untested by definition. - No CI test execution. The only GitHub Actions workflow (
ci-lint.yml) runsgolangci-lint— there is nogo teststep in CI. Tests are not automatically run on pull requests. app/testing/is a placeholder. Theapp/testing/testing.goandapp/testing/integration/integration.gofiles contain only the package declaration — no shared test helpers, fixtures, or builder utilities exist for the core application. Each test package reinvents its own mocks.- No database integration tests with testcontainers. The database layer (SQLite/PostgreSQL) is tested with in-memory mock structs rather than against a real schema. Schema migration correctness and SQL query results are not validated in any automated test.
- Inconsistent mock strategy. The registry sub-module uses mockery-generated mocks; the core app uses hand-written functional mocks; some tests use neither. This inconsistency increases maintenance burden.
- Integration test requires manual setup. The one integration test hits
localhost:8080with no test harness to provision or tear down the service — it will fail silently in any environment without the right pre-conditions.
Patterns worth emulating (for the book)#
- Table-driven tests with panic recovery (
dbtx/runner_test.go:130-141): the test wraps the call in an anonymous function withdefer recover()to test that panics inside transactions trigger rollback. A clean pattern for testing error paths that involvepanic. - Compile-time interface assertions in test files (
var _ Interface = (*Mock)(nil)): zero runtime cost, catches interface drift atgo buildtime. testify/mockwithmock.MatchedBy(upload_test.go:99): using a predicate function to assert on error message content without exact string matching — more resilient thanmock.Anythingbut less brittle than exact equality.waitServerretry helper (middleware_int_test.go:240): the exponential-backoff server readiness poll witht.Helper()decoration is a clean pattern for any integration test that starts a background service.