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_testpackage style is not used. - Helper packages:
client/orm/mock— Custom ORM mock infrastructure. ProvidesStartMock()/defer s.Clear()session lifecycle,MockRead,MockDelete,MockInsert, etc. helpers, and anInvocationinterceptor 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 abeecontext.Contextbacked byhttptest.ResponseRecorder, allowing controller logic to be tested without starting a real HTTP server. Also providesmock.Sessionfor session mocking.client/httplib/mock— HTTP client mock.mock.goprovidesMockFilterandMockConditiontypes 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 inTestMainorSetupSuite.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:embedfound.
Test patterns#
Table-driven tests#
- Prevalence: Moderate — 94 instances of
testCases/tt.Run/tc.nameacross 134 test files. - Style: Named struct slices, typically
[]struct{ name string; ... }, iterated witht.Run(tc.name, ...). Nomap[string]struct{}style. - Example:
client/orm/ddl_test.go:61— table of expected DDL SQL per database driver (MySQL, Postgres), each as a namedTestCasestruct withmodel,wantSQL,wantErrfields.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 anInvocationinterceptor into the ORM’s execution chain. Tests register expectations vias.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.ResponseRecorderpattern. Tests call handler functions directly with this mock context and inspect the recorder. - httplib mocking:
mock.MockFilterintercepts 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/httplib—HttplibTestSuiteembedssuite.Suite, withSetupSuite()starting a localnet.Listenserver andTearDownSuite()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
TestXxxfunctions.
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.gofiles. 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 viaInvocationcallbacks. 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.NewMockContexthelper makes it trivial to test controller logic in isolation. This is a pattern that many framework users will copy. - testify’s
assert/requireproduce clear failure messages compared to rawt.Fatalcomparisons — the codebase has adopted this consistently in newer packages.
- The ORM mock infrastructure (
What could improve:
- Test placement is exclusively white-box (same package). Using
_testpackage 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 integrationguard 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,TestFooZfunctions 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
TestMainsetup/teardown is currently done via package-levelinit().
- Test placement is exclusively white-box (same package). Using
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. NewMockContextas a first-class public helper in amocksub-package (not a_test.gofile) lets application code import it in their own tests — an explicit acknowledgment that the framework should be testable by users, not just internally.
- The