Gogs — Testing#

Test metrics#

  • Test files: 66
  • Source files: 288 (non-vendor .go files)
  • Ratio (test files / source files): ~0.23 (roughly 1 test file per 4.4 source files)
  • Test frameworks: github.com/stretchr/testify (assert + require); no ginkgo/gocheck/goconvey

Test organization#

  • Placement: Overwhelmingly same-package (white-box) — 63 of 66 test files declare the production package name; only 3 use the _test external package suffix.
  • Helper packages:
    • internal/testx — project test utilities: InTest flag (detects test binary at runtime), golden file assertion via AssertGolden with an -update flag, a noop logger factory, and exec helpers. Thin and focused.
    • internal/dbtest — multi-engine test database scaffolding. NewDB(t, suite, tables...) creates an isolated real database (SQLite3 by default; MySQL or PostgreSQL via GOGS_DATABASE_TYPE env var), auto-migrates requested tables, and registers cleanup via t.Cleanup. The DB is left intact on failure for inspection.
    • internal/database/testdata/, internal/testx/testdata/, internal/conf/testdata/ — fixture files (SQL, INI, golden JSON) embedded or loaded by tests.
  • Fixtures: testdata/ directories at three locations; golden files are stored as JSON or raw strings and regenerated with go test -update=<regex>. The -update flag is parsed via flag.String in testx.golden.go before TestMain runs.

Test patterns#

Table-driven tests#

  • Prevalence: Heavy use — 236 matches for t.Run, tt.Run, tc.name, and testCases in test files.
  • Style: Named struct slices ([]struct{ name string; ... }) with t.Run(tc.name, ...) subtest execution; outer loop pattern with if t.Failed() { break } to abort after first failure in ordered store test suites.
  • Example: internal/database/access_tokens_test.go:100TestAccessTokens registers subtests as a table of {name, func} pairs and runs them sequentially, clearing tables via t.Cleanup between each. Subtests are extracted into named functions (e.g., accessTokensCreate) to keep the table readable.

Mocking approach#

  • Strategy: Code-generated mocks via go-mockgen (sourcegraph’s fork), not gomock or mockery. Mock structs expose per-method hook queues (PushHook, SetDefaultHook) and call history (History()). Both lenient and strict variants are generated (NewMock*, NewStrictMock*).
  • Config: mockgen.yaml at repo root declares which interfaces to mock and where to write output — by convention always into mocks_test.go (test-only) or mocks_gen.go (production, when mocking auth providers).
  • Generated mocks:
    • internal/database/mocks_test.goMockLoginSourceFileStore, MockLoginSourceFilesStore (unexported interfaces, tested internally).
    • internal/database/mocks_gen.goMockAuthProvider (exported, for cross-package use).
    • internal/route/lfs/mocks_test.goMockStore for the LFS Store interface.
  • Assessment: go-mockgen’s queue hook model is more explicit and verifiable than gomock’s call-expectation DSL. Mocks record every invocation’s args and results via the History() accessor, enabling post-call assertions without declaring expectations upfront.
  • Example: internal/route/lfs/batch_test.goTestServeBatch constructs a MockStore, uses httptest.NewRecorder and macaron.Classic() to drive the LFS batch handler end-to-end, then asserts on HTTP status codes and body JSON.

Integration tests#

  • Present: Yes — the internal/database/ store tests hit a real database engine, not stubs.
  • How: internal/dbtest.NewDB provisions a fresh SQLite3 file (default), MySQL database, or PostgreSQL database depending on GOGS_DATABASE_TYPE. Tables are created via gorm.AutoMigrate and dropped on teardown. NowFunc is fixed to time.Now().UTC().Truncate(time.Second) to make timestamp assertions deterministic.
  • Separation: Database-hitting tests guard with testing.Short() (14 occurrences) — running with -short skips them, enabling a fast unit-only pass.
  • CI matrix: Three separate GitHub Actions jobs — test (SQLite3, Ubuntu + macOS + Windows), postgres (PostgreSQL 9.6 via service container), mysql (MySQL via systemd). All run with -race on Unix; Windows omits -race due to golang/go#46099.

Test quality observations#

What’s done well#

  • Real-database integration tests with multi-engine support. The dbtest.NewDB scaffolding is a clean, reusable pattern: a single call provisions a fully-migrated isolated DB, registers teardown, and supports three engines via env-var selection. Tests find regressions that in-memory fakes would miss.
  • Generated mocks with invocation history. go-mockgen’s queue hook pattern enables both behavior injection and post-call verification without fragile expectation ordering. The PushHook / History() API is testable without a matching framework.
  • Golden file testing. testx.AssertGolden with -update regex makes it trivial to regenerate expected output when changing serialization behavior. Skipping on Windows is a pragmatic choice given line-ending differences.
  • Deterministic time. Injecting a fixed NowFunc into GORM at test setup eliminates flakiness from timestamp comparisons — a detail many projects miss.
  • Shuffled, raced CI. The go test -shuffle=on -race invocation in CI catches both ordering-dependent test pollution and data races simultaneously.
  • Readable table construction. Extracting each subtest’s body into a named function (e.g., accessTokensCreate) avoids anonymous-function soup while keeping the dispatch table compact.

What could improve#

  • Coverage of HTTP route handlers is sparse. Only the LFS route package has systematic handler tests (batch_test.go, basic_test.go). The large internal/route/ tree (user, repo, org, admin handlers) has only webhook_test.go — most request handlers are untested at the HTTP layer.
  • Legacy xorm model layer is essentially untested. The internal/database/ files still backed by xorm (models_*.go, repo.go, user.go) have no corresponding test files; coverage comes only from the newer GORM store layer.
  • Low test-to-source ratio for a project of this age. 0.23 is acceptable for a utilities-heavy codebase but reflects the historical testing gap in the legacy layer.
  • No test for the Macaron middleware pipeline. context.Contexter, context.RepoAssignment, context.Toggle — the middleware that sets up request context — are untested in isolation.

Patterns worth emulating#

  • dbtest.NewDB as a reusable multi-engine test harness — the pattern of reading engine type from an env var, creating a uniquely-named test database, auto-migrating only the needed tables, and registering cleanup via t.Cleanup is directly transferable to any GORM-backed project.
  • go-mockgen queue hooks for interface mocking — the PushHook / SetDefaultHook / History() triad is a stateful, verifiable alternative to gomock’s expectation-based model; worth considering when test authors find gomock’s EXPECT() chains fragile.
  • Golden files with per-test -update flag — the testx.AssertGolden + flag.String("update", ...) pattern lets test authors regenerate specific golden files selectively without a blanket update command that might mask regressions.