Gitea — Testing#
Test metrics#
- Test files: 898
*_test.gofiles - Source files (non-test): 1,977
.gofiles - 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, ortc.namepatterns
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
_testpackage (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 asqlite3 file::memory:database, syncs the full schema via xorm, and loads YAML fixtures viaInitFixtures()MainTest(m *testing.M, opts)is the universal entry point for any test that needs a database. Packages callunittest.MainTest(m)in theirTestMain; 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.Contextbacked byhttptest.ResponseRecorder. Used for unit-testing web handlers without starting a full server.MockRender{}is a no-opRenderimplementation used when template rendering isn’t under test.
tests/ (root package)
InitTest()boots the full application (callsrouters.InitWebInstalled()), used by integration testsPrepareTestEnv(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 starttests/gitea-lfs-meta/holds LFS object fixtures for storage teststestdata/subdirectories inmodules/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 withtreePath,fileContent,outcomes,expectedStatusesfields; 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{}— implementshttp.RoundTripperto intercept HTTP calls in tests, injected via functional optionWithHTTP()tests/integration/actions_runner_test.go:mockRunner/mockRunnerClient— implement the actions runner proto interface to simulate a real runner registering and completing jobstests/integration/repo_webhook_test.go:mockWebhookProvider— implements the webhook provider interface to capture fired webhooksmodules/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.gocallstests.InitTest()which boots the full Gitea server (all routers, middleware, and services) in-process. Tests useNewRequest+MakeRequesthelpers built onhttptest.NewRecorder(). The full cookie/session lifecycle is exercised viaTestSession. - 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-e2ebuilds a test binary;make test-e2eruns Playwright against it) - Timeout factor: Configurable via
GITEA_TEST_E2E_TIMEOUT_FACTORenv var
Fuzz tests#
- Present: Yes — 2 fuzz targets in
tests/fuzz/fuzz_test.go:FuzzMarkdownRenderRawandFuzzMarkupPostProcess - 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:
| Workflow | Trigger | Databases | Notes |
|---|---|---|---|
pull-db-tests.yml | PR | pgsql, sqlite, mysql, mssql | Each DB is a separate job with Docker service containers (ldap, minio also spun up for pgsql) |
pull-e2e-tests.yml | PR | n/a | Playwright, Chromium + Firefox in CI |
pull-compliance.yml | PR | n/a | Linting 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/unittestpackage 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/contexttestpackage: 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
-raceon 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) andrequire(immediate stop) are used with 4,788 combinedNoErrorcalls. 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
MockLocaleinmodules/translation/mock.gobeing in a non-test file is a notable smell. - 238 packages each calling
unittest.MainTest: Every DB-touching package has its ownTestMain. This is correct Go practice but means test setup is replicated 238 times, and a change tounittest.TestOptionsimpacts all of them. - Integration test isolation:
PrepareTestEnvreloads fixtures per test, but the fullInitTest()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. TheTestOptions.SetUp/TearDownhooks 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.