Gitea — Testing#

Test metrics#

  • Test files: 898 *_test.go files
  • Source files (non-test): 1,977 .go files
  • Ratio (test / source): ~0.45 — roughly one test file per 2.2 source files
  • Total test functions: 2,278 func Test* functions
  • Test frameworks: testify/assert + testify/require (984 import occurrences across test files); no gomock, ginkgo, gocheck, or goconvey
  • Table-driven tests: 242 occurrences of testCases, tt.Run, or tc.name patterns

Test organization#

Placement#

Both strategies are used:

  • Same-package (white-box): ~793 files — tests live in the same package as the code they test, granting access to unexported identifiers
  • External _test package (black-box): 105 files — used where public API surface is the right boundary, especially in integration test packages

Helper packages#

Three dedicated helper packages form the testing infrastructure backbone:

models/unittest (most important)

  • CreateTestEngine() spins up a sqlite3 file::memory: database, syncs the full schema via xorm, and loads YAML fixtures via InitFixtures()
  • MainTest(m *testing.M, opts) is the universal entry point for any test that needs a database. Packages call unittest.MainTest(m) in their TestMain; 238 packages do this.
  • Typed assertion helpers: AssertExistsAndLoadBean[T], AssertNotExistsBean, AssertCount, AssertCountByCond — query the live test DB and produce readable failure messages
  • GetBean[T] uses generics for type-safe DB lookups in tests (Go 1.21)
  • PrepareTestDatabase() reloads fixtures between tests for isolation

services/contexttest

  • MockContext(t, "/path GET") constructs a fully wired *context.Context backed by httptest.ResponseRecorder. Used for unit-testing web handlers without starting a full server.
  • MockRender{} is a no-op Render implementation used when template rendering isn’t under test.

tests/ (root package)

  • InitTest() boots the full application (calls routers.InitWebInstalled()), used by integration tests
  • PrepareTestEnv(t) loads fixtures + syncs git repo fixtures for each integration test
  • Specific Prepare* functions: PrepareAttachmentsStorage, PrepareGitRepoDirectory, PrepareLFSStorage, PrepareCleanPackageData — called selectively per test

Fixtures#

  • 78 YAML fixture files in models/fixtures/ covering every model (users, repos, issues, pull requests, actions, packages, etc.)
  • Fixtures use a custom dummy password hasher (hash.Register("dummy", ...)) so stored password hashes are deterministic and fast
  • tests/gitea-repositories-meta/ contains bare git repository fixtures synced into temp dirs at test start
  • tests/gitea-lfs-meta/ holds LFS object fixtures for storage tests
  • testdata/ subdirectories in modules/avatar/, modules/storage/, modules/actions/jobparser/ hold binary/file fixtures for specific module tests

Test patterns#

Table-driven tests#

  • Prevalence: Heavy — 242 occurrences. The dominant pattern throughout the codebase.
  • Style: Anonymous struct slices with named fields are standard; sometimes named struct types for complex cases.
  • Example: tests/integration/actions_job_test.go:TestJobWithNeeds — anonymous struct slice with treePath, fileContent, outcomes, expectedStatuses fields; each row creates a git workflow file and asserts CI job outcomes.

Mocking approach#

  • Strategy: Manual interface fakes. No mock-generation framework (no gomock/mockery).
  • Examples:
    • modules/hcaptcha/hcaptcha_test.go:mockTransport{} — implements http.RoundTripper to intercept HTTP calls in tests, injected via functional option WithHTTP()
    • tests/integration/actions_runner_test.go:mockRunner / mockRunnerClient — implement the actions runner proto interface to simulate a real runner registering and completing jobs
    • tests/integration/repo_webhook_test.go:mockWebhookProvider — implements the webhook provider interface to capture fired webhooks
    • modules/translation/mock.go:MockLocale — production-level fake locale for tests requiring i18n (not generated from an interface tool)
    • services/contexttest.MockContext — hand-rolled HTTP context factory, not a stub library

The philosophy is: use a real sqlite3 in-memory database rather than mocking DB calls, and use a real in-process HTTP server rather than mocking handlers.

Integration tests#

  • Present: Yes — 235 test files in tests/integration/
  • How: integration_test.go calls tests.InitTest() which boots the full Gitea server (all routers, middleware, and services) in-process. Tests use NewRequest + MakeRequest helpers built on httptest.NewRecorder(). The full cookie/session lifecycle is exercised via TestSession.
  • Database: Real database — SQLite3 for local runs, PostgreSQL/MySQL/MSSQL in CI (see below)
  • Separation: The tests/integration/ directory is its own Go package (package integration), separated from unit tests by directory rather than build tags
  • Scope: Integration tests cover the full HTTP API (web UI routes + REST API + actions runner API via gRPC/Connect-RPC), including auth, git operations, packages, webhooks, and CI/CD flows

E2E tests (Playwright)#

  • Present: Yes — 13 TypeScript test files in tests/e2e/ covering login, register, repo operations, milestone, org, events, and user settings
  • Framework: Playwright with Chromium (local) and Chromium + Firefox (CI)
  • How: Tests run against a real running Gitea binary (make gitea-e2e builds a test binary; make test-e2e runs Playwright against it)
  • Timeout factor: Configurable via GITEA_TEST_E2E_TIMEOUT_FACTOR env var

Fuzz tests#

  • Present: Yes — 2 fuzz targets in tests/fuzz/fuzz_test.go: FuzzMarkdownRenderRaw and FuzzMarkupPostProcess
  • Scope: Narrow — only markup/markdown rendering is fuzzed, which is a reasonable attack surface given Gitea’s role as a code forge with rich markup rendering

CI configuration#

Separate GitHub Actions workflows per test tier:

WorkflowTriggerDatabasesNotes
pull-db-tests.ymlPRpgsql, sqlite, mysql, mssqlEach DB is a separate job with Docker service containers (ldap, minio also spun up for pgsql)
pull-e2e-tests.ymlPRn/aPlaywright, Chromium + Firefox in CI
pull-compliance.ymlPRn/aLinting and format checks

Race detector is enabled for PostgreSQL integration tests (RACE_ENABLED: true).

Test quality observations#

What’s done well#

  • Real database over mocks: Using sqlite3 in-memory for unit tests and real DB services in CI catches actual ORM/SQL bugs rather than hiding them behind stubs. This strategy has clearly paid off — the models/unittest package is mature and well-used.
  • Fixture depth: 78 YAML fixture files covering every domain object. New tests can easily set up realistic data scenarios.
  • The services/contexttest package: Enables handler-level unit tests without a full server boot, filling the gap between pure unit tests and heavy integration tests.
  • Three-tier test pyramid: Unit tests (sqlite3 in-memory + testify), integration tests (in-process full server + httptest), and E2E tests (Playwright against a real binary) are clearly distinguished.
  • Race detector in CI: Running -race on pgsql integration tests actively catches concurrency bugs in a codebase that has significant goroutine usage.
  • Fuzz targets for markup: Markdown rendering is a historically bug-prone area in forges; having fuzz targets here is prudent.

What could improve#

  • assert vs require inconsistency: Both assert (test continues after failure) and require (immediate stop) are used with 4,788 combined NoError calls. There’s no consistent policy on when to use which; some tests continue running after a fatal error, producing confusing cascading failures.
  • No mock generation: Manual fakes work but drift silently when interfaces change. A code-generated approach (mockery or gomock) would catch interface-fake mismatches at compile time. The MockLocale in modules/translation/mock.go being in a non-test file is a notable smell.
  • 238 packages each calling unittest.MainTest: Every DB-touching package has its own TestMain. This is correct Go practice but means test setup is replicated 238 times, and a change to unittest.TestOptions impacts all of them.
  • Integration test isolation: PrepareTestEnv reloads fixtures per test, but the full InitTest() server boot only happens once per test binary run. Stateful global singletons (graceful manager, queue manager) can leak state between integration tests in non-obvious ways.

Patterns worth emulating (for the book)#

  • models/unittest.MainTest + YAML fixtures pattern: A clean, reusable recipe for packages that need a real database in tests. The TestOptions.SetUp/TearDown hooks let packages customize initialization without forking the setup logic.
  • services/contexttest.MockContext: A hand-crafted HTTP context mock that eliminates most infrastructure from handler unit tests. Simpler and less brittle than trying to mock at the repository/service layer.
  • Three-tier pyramid with explicit directories: models/ unit tests, tests/integration/ for HTTP integration, tests/e2e/ for browser E2E — each tier has a clear purpose and location. No ambiguity about what kind of test belongs where.
  • Fixture files for git repos: Checking in bare git repository fixtures (tests/gitea-repositories-meta/) synced into temp dirs at test start enables realistic git operation tests without running a live git remote.