Echo — Testing#
Test metrics#
- Test files: 46
- Source files (non-test): 44 (root + middleware combined)
- Ratio (test files / source files): ~1.05 — effectively 1:1 parity; almost every source file has a dedicated test file
- Test frameworks:
testify/assertandtestify/require(stdlibtestingas the base); no mocking framework
Test organization#
Placement#
Both internal (same package) and external (_test suffix package). Most root-level tests use package echo (white-box), while 4 files use package echo_test or package echotest_test (black-box). The echotest/ sub-package has internal and external variants (context_test.go vs context_external_test.go). Middleware tests all use package middleware (white-box).
The deliberate mix signals intent: white-box tests for internals (binder dispatch, router tree internals), black-box tests for the public API surface (to catch regressions in exported contracts).
Helper packages#
echotest/ — a first-class public testing helper package shipped with the framework:
context.go—ContextConfigstruct with builder-style API for constructingecho.Contextvalues for tests. Supports setting request, response, query params, headers, form values, multipart forms, JSON body, path values, and route info.ToContext(t),ToContextRecorder(t), andServeWithHandler(t, handler)are the three primary methods.reader.go—LoadBytes(t, name)reads fixture files relative to the calling test’s source directory usingruntime.Caller(2)to resolve the path.TrimNewlineEndis a functional option for stripping trailing newlines. This allows test fixtures to live next to the test file, not at a fixed absolute path.
Fixtures#
echotest/testdata/— fixture files forechotestpackage testsmiddleware/testdata/— containstest.json, adist/directory, andprivate.txtfor static file serving tests- No code generation or embedded fixture binaries; fixtures are plain files loaded at test time
Test patterns#
Table-driven tests#
- Prevalence: Extremely heavy — 648 matches for
t.Run(/testCases/tests :=in test files - Style: Anonymous struct with descriptive field names. Consistent
given/when/expectfield naming:givenConfig,whenMethod,whenHeaders,expectHeaders,expectErr(visible incors_test.go:37–43). This naming scheme makes test case intent self-documenting. - Example:
middleware/cors_test.go:36—TestCORSConfigwith 20+ cases covering wildcard origins, credentials, preflight, vary headers, and edge cases. Each case isname + givenConfig + whenXxx + expectXxxfields.
Mocking approach#
- Strategy: No mocking framework. Two patterns are used:
net/http/httptestdirectly —httptest.NewRequest+httptest.NewRecorderare used 640 times across test files. This is the dominant approach for all HTTP-level tests.echotest.ContextConfig— higher-level wrapper aroundhttptestthat removes boilerplate for constructing rich contexts (forms, multipart, JSON bodies, path params). Shipped as a public API for end users writing handler tests.
- No
gomock,mockery, or hand-written mock structs. Dependencies are swapped at theEcho.Configlevel (e.g., substituting a customLogger,Binder, orRenderer) using the framework’s own interface slots — tests exercise real implementations, not mocks.
Integration tests#
- Present: Yes, functionally (no separate
_integration_test.gofiles by name) - How: The middleware tests in
middleware/start a realecho.New()instance and run handlers through the full middleware chain usinghttptest. These tests exercise binding, error handling, header propagation, and response writing end-to-end — more integration than unit.echotest.ContextConfig.ServeWithHandlersimilarly runs the full handler + error handler path. - Separation: No build tags or separate directories. Integration-style tests are co-located with unit tests. The distinction is implicit (table-driven tests with many cases covering end-to-end scenarios vs. narrow unit tests of internals).
Concurrency tests#
router_concurrent_test.go— dedicated file for concurrent router correctness (TestConcurrentRouter_ConcurrentReadslaunches 10 goroutines withsync.WaitGroup+atomic.Int64counters). TestsConcurrentRouterwrapper under concurrent reads/writes.t.Parallel()is used in 14 test functions.- CI runs all tests with
-race(make race) as a required step.
Benchmarks#
- 10+ benchmark functions in
router_test.goandbinder_test.go. - Router benchmarks use real-world route tables: static routes, GitHub API routes (
BenchmarkRouterGitHubAPI), with and without cache misses. - Binder benchmarks compare
DefaultBindervsValueBinderfor single and 10-field cases. make benchmarkis a documented Makefile target.
CI configuration#
GitHub Actions (.github/workflows/):
- checks workflow:
golint,staticcheck,govulncheckon the latest Go version - tests workflow: Matrix of Go versions; runs
go test ./...with race detector - No test coverage enforcement visible (no coverage thresholds or coverage upload steps)
Test quality observations#
What’s done well#
- 1:1 test parity — nearly every source file has a test file. Coverage is comprehensive.
- Public
echotestpackage — Echo ships testing helpers as a first-class library feature. This is unusual and highly valuable: it means downstream users can write clean handler tests without reinventinghttptestboilerplate. - Consistent table-driven style — the
given/when/expectfield naming convention across middleware tests reads almost like a BDD spec. Each test case name describes the scenario in plain English. - Benchmarks as first-class citizens — router and binder benchmarks use real API route tables (Go stdlib routes, GitHub API routes), not synthetic microbenchmarks. Combined with
make raceandmake benchmarkin CI, performance regressions are treated as bugs. - No mock sprawl — avoiding mock frameworks keeps tests readable and close to real behavior. The framework’s own interface-slot DI pattern makes test setup clean without needing mocks.
runtime.Callerfor fixture paths —LoadBytesresolves fixture paths relative to the source file, not the working directory. This is a subtle but important correctness improvement for monorepo or nested test scenarios.
What could improve#
- No coverage thresholds in CI — there is no
go test -coverprofileor coverage gate. The repo relies on test comprehensiveness by convention rather than enforcement. - No explicit separation of integration tests — mixing unit and integration-style tests in the same files makes selective test execution (e.g.,
go test -short) harder. A build tag (//go:build integration) or naming convention would add clarity. t.Parallel()underused — only 14 uses across 46 test files. Given the framework’s concurrency focus, more parallelized tests could catch races earlier in CI.
Patterns worth emulating#
- Shipping
echotestas a public package — frameworks that provide official test helpers dramatically reduce the cost of writing good tests for users. TheContextConfig.ServeWithHandlerpattern in particular is a clean, reusable handler testing primitive. given/when/expectnaming in anonymous struct test cases — self-documenting table test fields that read as specifications.- Using the framework’s own DI slots for test customization — instead of mocking, substitute real test-specific implementations via
Config{}. This keeps tests honest and avoids test-only code paths. - Benchmark tables with real-world data — using Go stdlib and GitHub API route sets as benchmark inputs makes performance numbers meaningful and comparable across versions.