Buffalo — Testing#

Test metrics#

  • Test files: 51
  • Source files (non-test): ~98 (149 total .go files minus 51 test files)
  • Ratio (test files / source files): ~0.52 — reasonable coverage for a framework
  • Test frameworks: github.com/stretchr/testify/require (universal), stdlib testing, github.com/gobuffalo/httptest (HTTP integration helper)

Test organization#

  • Placement: Same package (white-box). All core tests use package buffalo (not package buffalo_test), giving direct access to unexported fields. Sub-packages (binding, render, worker, plugins, mail) mirror the same same-package convention.
  • Helper packages:
    • internal/testdata/ — two subdirectories: disk/ (file-system fixtures) and embedded/ (FS embed fixtures), plus a panic.txt for error-rendering tests. Used by fs_test.go to test both disk-based and embedded template loading.
    • gobuffalo/httptest — an external Buffalo-owned library that wraps net/http/httptest with a fluent API (w.HTML("/path").Get()). Also provides httptest.NewServer for full-server tests.
    • No testutil, mock, or fake package in the repo itself.
  • Fixtures: internal/testdata/ directory; render tests use testing/fstest.MapFS (in-memory virtual FS) for fast, self-contained template setup.

Test patterns#

Table-driven tests#

  • Prevalence: Occasional — 9 occurrences of table markers (tests :=, testCases, tt.Run, tc.name). Used selectively, not universally.
  • Style: Mixed. Two styles appear:
    • Anonymous struct slice — most common when multiple fields are needed (e.g., Test_Resource in router_test.go:504, Test_PreHandlers:186).
    • map[string]string — used for input → expected-output mappings where iteration order doesn’t matter (e.g., Test_buildRouteName:729, Test_Mount_Buffalo:87).
  • Example: router_test.go:510tests := []trs{...} with Method, Path, Result fields, iterated with for _, test := range tests.
  • Sub-tests (t.Run): Used in Test_Router_Matches_Trailing_Slash (router_test.go:799) where each table row gets an isolated sub-test with a descriptive name (tt.mapped+"|"+tt.browser). Not used broadly.

Mocking approach#

  • Strategy: No mocking framework (no gomock, mockery). Instead, Buffalo uses concrete in-test fake structs that implement the relevant interface.
    • userResource (router_test.go:632) — a struct with all seven REST resource methods implemented with simple string renders, used to test the Resource() registration API.
    • paramKeyResource / mwResource / WebResource — further inline resource fakes that override specific methods to test edge-case behaviour (custom param keys, middleware on resources, not-implemented defaults).
    • Worker tests (worker/simple_test.go) call the real Simple implementation directly; no mock worker is needed because the worker has no external dependencies.
  • Example: router_test.go:632-660userResource implements Resource interface with all methods; its methods return render.String(...) literals that tests assert on.

Integration tests#

  • Present: Yes — two layers.
    1. HTTP integration via gobuffalo/httptest: Most tests in router_test.go, middleware_test.go, errors_test.go, etc. start a real *App and make real HTTP calls through httptest.New(a) (in-process test server, no port) or httptest.NewServer(a) (live TCP listener). This gives near-production fidelity without network setup.
    2. Full lifecycle tests in server_test.go: Test_Server_GracefulShutdownOngoingRequest and Test_Server_GracefulShutdownOngoingWorker actually call app.Serve() in a goroutine, hit the real http://127.0.0.1:3000 endpoint, then call app.cancel() and assert that in-flight requests complete while new connections are refused. These tests use time.Sleep (2-8 seconds) for synchronisation — acknowledged in comments as timing-sensitive.
  • How: In-process (httptest.New) for unit-style HTTP tests; live TCP (httptest.NewServer, http.Get("http://127.0.0.1:3000")) for lifecycle tests. No Docker or testcontainers.
  • Separation: No build tags. Lifecycle tests are in server_test.go alongside lighter tests. Tests are distinguished by what they do, not by file naming or tags.

Test quality observations#

What’s done well#

  • Integration-first philosophy. Buffalo’s tests consistently spin up a real *App and exercise it through actual HTTP. This means tests catch wiring bugs (wrong middleware order, route registration errors, response encoding issues) that unit tests with mocks would miss. The framework tests its own framework contracts.
  • gobuffalo/httptest fluent API. w.HTML("/foo").Get(), w.HTML("/foo").Post(body) makes HTTP test setup extremely concise and readable. The helper is a pattern worth studying for any HTTP framework test suite.
  • In-memory FS for render tests. testing/fstest.MapFS is used in render/render_test.go:18 and several render sub-tests. This avoids touching the real filesystem while still exercising template loading paths end-to-end.
  • Inline resource fakes over mocks. Defining userResource in the test file is more readable than a generated mock for an interface with 7 methods. The implementations are trivially correct and serve as documentation of what a resource is expected to do.
  • Graceful shutdown tests. Testing that in-flight requests complete after app.cancel() is uncommon and high-value. Most frameworks skip this; Buffalo has explicit tests for it.
  • Sub-test naming. Test_Router_Matches_Trailing_Slash uses t.Run(tt.mapped+"|"+tt.browser, ...) making failure output immediately actionable.

What could improve#

  • time.Sleep in lifecycle tests. server_test.go relies on time.Sleep(2*time.Second) to wait for server startup and time.Sleep(1*time.Second) between steps. This is fragile on slow CI and makes the test suite unnecessarily slow (~12 seconds just for the graceful-shutdown tests). A channel-based or polling-based readiness check would be more reliable.
  • Low table-driven test prevalence. Only 9 table-driven occurrences across 51 test files. Many tests repeat similar setup patterns (create App, add route, call via httptest, assert). Consolidating these into subtests would reduce boilerplate and make coverage gaps visible at a glance.
  • No coverage of error-path rendering. The embedded error templates (devErrorTmpl, prodErrorTmpl) are tested in errors_test.go, but only for status code and gross content. The Plush-rendered error HTML is not asserted in detail, so template typos could ship undetected.
  • Absence of _test (black-box) packages. All tests are white-box (package buffalo). For a library, this makes it harder to catch API surface regressions (accidentally removed exports, changed signatures). Adding at least a few package buffalo_test tests for the most-used public API entry points would improve this.

Patterns worth emulating#

  1. Integration-first HTTP tests with an in-process test server. httptest.New(app) gives full stack coverage with minimal setup; this pattern should be the default for any Go HTTP framework or API server.
  2. Lifecycle tests for graceful shutdown. Even with the time.Sleep weakness, the intent — test that the server drains in-flight requests — is a pattern every HTTP server should exercise. Combine with channel-based readiness for production-quality test design.
  3. testing/fstest.MapFS for template/FS tests. Replacing on-disk fixtures with in-memory FS is simpler to maintain, runs faster, and avoids accidental test pollution from leftover files.