Beego — Testing#

Test metrics#

  • Test files: 134
  • Total Go files: 363
  • Ratio (test files / source files): ~37% (roughly 1 test file per 2.7 source files)
  • Test frameworks: stdlib testing (universal), testify (assert, require, suite) dominant in newer packages

Test organization#

  • Placement: Same package (white-box testing is the norm). Test files are co-located with source in the same directory and use the same package declaration (e.g., package web, package orm, package httplib). External _test package style is not used.
  • Helper packages:
    • client/orm/mock — Custom ORM mock infrastructure. Provides StartMock() / defer s.Clear() session lifecycle, MockRead, MockDelete, MockInsert, etc. helpers, and an Invocation interceptor that captures method calls. Internally uses SQLite for low-fidelity “real DB” tests alongside the interceptor mechanism.
    • server/web/mock — Web layer mock helpers. NewMockContext(req) creates a beecontext.Context backed by httptest.ResponseRecorder, allowing controller logic to be tested without starting a real HTTP server. Also provides mock.Session for session mocking.
    • client/httplib/mock — HTTP client mock. mock.go provides MockFilter and MockCondition types for intercepting outbound HTTP calls during tests.
    • client/httplib/testing — A pre-configured test client (client.go) for integration-style tests against a local HTTP server started in TestMain or SetupSuite.
    • core/utils/testdata — Static fixture files for utility function tests.
  • Fixtures: testdata/ directories hold static fixtures (e.g., config files, templates). No embedded fixtures via //go:embed found.

Test patterns#

Table-driven tests#

  • Prevalence: Moderate — 94 instances of testCases/tt.Run/tc.name across 134 test files.
  • Style: Named struct slices, typically []struct{ name string; ... }, iterated with t.Run(tc.name, ...). No map[string]struct{} style.
  • Example: client/orm/ddl_test.go:61 — table of expected DDL SQL per database driver (MySQL, Postgres), each as a named TestCase struct with model, wantSQL, wantErr fields. client/httplib/httplib_test.go:375 — table of HTTP request scenarios in a testify suite.

Mocking approach#

  • Strategy: Custom hand-rolled mocks rather than gomock or mockery. Beego provides its own mock packages (see above) designed specifically for its interfaces.
  • ORM mocking: StartMock() installs an Invocation interceptor into the ORM’s execution chain. Tests register expectations via s.Mock(MockRead(...)), then call ORM methods normally. On invocation, the interceptor matches by table name + method name and returns pre-configured responses. This avoids needing gomock interface mocks for every ORM interface method.
  • HTTP context mocking: server/web/mock.NewMockContext(req) + httptest.ResponseRecorder pattern. Tests call handler functions directly with this mock context and inspect the recorder.
  • httplib mocking: mock.MockFilter intercepts outbound HTTP calls at the filter layer — no real network I/O needed for unit tests of HTTP client logic.
  • Example (ORM mock):
    // client/orm/mock/mock_orm_test.go
    s := StartMock()
    defer s.Clear()
    s.Mock(MockDeleteWithCtx((&User{}).TableName(), 12, nil))
    o := orm.NewOrm()
    rows, err := o.Delete(&User{})
    assert.Equal(t, int64(12), rows)
    assert.Nil(t, err)

Testify suite pattern#

  • Used in: client/httplibHttplibTestSuite embeds suite.Suite, with SetupSuite() starting a local net.Listen server and TearDownSuite() closing it. Test methods are suite methods (func (h *HttplibTestSuite) TestGet()), allowing shared server state across test cases.
  • Prevalence: Confined to httplib; other packages use plain TestXxx functions.

Integration tests#

  • Present: Yes — ORM tests are real integration tests run against live databases.
  • How: GitHub Actions CI spins up Docker service containers for MySQL, PostgreSQL, Redis, Memcached, etcd, and SSDB. ORM tests are run three times: once against sqlite3 (local file), once against PostgreSQL, once against MySQL. Cache tests hit real Redis/Memcached/SSDB/etcd endpoints.
  • Separation: No build tags or separate directories. Integration tests are mixed with unit tests in the same *_test.go files. Database driver is selected via environment variables (ORM_DRIVER, ORM_SOURCE), so the same test file acts as a unit test (sqlite3, in-process) or integration test (MySQL/Postgres in CI). This is an unusual and pragmatic approach — the tests adapt to whatever backend is available.
  • Coverage upload: CI uploads coverage reports to codecov after the full MySQL run (which covers all packages).

Test quality observations#

  • What’s done well:

    • The ORM mock infrastructure (client/orm/mock) is a thoughtful, bespoke solution. Rather than mocking every interface method, it intercepts at the execution layer via Invocation callbacks. This makes tests resilient to interface changes and reduces mock boilerplate significantly.
    • CI matrix is comprehensive for a framework: testing against 3 SQL databases and 4 cache backends in a single workflow ensures real compatibility rather than just mocked behavior.
    • The server/web/mock.NewMockContext helper makes it trivial to test controller logic in isolation. This is a pattern that many framework users will copy.
    • testify’s assert / require produce clear failure messages compared to raw t.Fatal comparisons — the codebase has adopted this consistently in newer packages.
  • What could improve:

    • Test placement is exclusively white-box (same package). Using _test package declarations for public API tests would catch accidental internal coupling and better document the public contract.
    • No build tags separate unit from integration tests. A developer running go test ./... locally without the Docker services will see failures in ORM and cache packages unless they have live backends. A //go:build integration guard on those tests would improve the local development experience.
    • Table-driven test adoption is moderate (94 instances) but not universal. Some packages have long sequences of similar TestFooX, TestFooY, TestFooZ functions that would be cleaner as a single table-driven test.
    • The testify suite pattern is used only in httplib — it would benefit several other multi-step packages (e.g., ORM, session) where TestMain setup/teardown is currently done via package-level init().
  • Patterns worth emulating:

    • The StartMock() / defer s.Clear() lifecycle pattern for stateful mock sessions is clean and safe. It ensures mock state never leaks between tests.
    • Driving database-backed integration tests via environment variables (ORM_DRIVER, ORM_SOURCE) allows the same test code to run in multiple modes without conditional compilation. This is a practical strategy for ORMs and adapters.
    • NewMockContext as a first-class public helper in a mock sub-package (not a _test.go file) lets application code import it in their own tests — an explicit acknowledgment that the framework should be testable by users, not just internally.