Fiber — Testing#

Test metrics#

  • Test files: 95 *_test.go files
  • Source files (non-test): 148 .go files
  • Ratio (test / source): ~0.64
  • Total test functions: 1,605 (func Test*)
  • Benchmark functions: 348 (func Benchmark*)
  • Fuzz functions: 1 (FuzzUtilsGetOffer)
  • t.Parallel() calls: 2,119 — virtually every test is parallel
  • Test frameworks: testify/require (primary), testify/assert (secondary), stdlib testing
  • No gomock, ginkgo, goconvey, or other third-party test frameworks

Test organization#

Placement#

Predominantly same package (internal white-box tests). The package breakdown:

PackageTest files
binder12
fiber (core)9
client9
session6
cors, csrf, cache, limiter, …2–4 each

Only 3 test files use the external _test package suffix — the most notable being app_integration_test.go (package fiber_test), which tests cross-cutting concerns from the consumer’s perspective.

Helper packages#

None in the traditional sense — no testutil/, mocks/, or fakes/ directory. Instead, Fiber relies on three mechanisms:

  • app.Test(req *http.Request) — the built-in integration test helper on the App type itself (see below)
  • fasthttputil.NewInmemoryListener() — borrowed from fasthttp’s test utilities to spin up a real in-memory TCP listener for end-to-end scenarios
  • Inline test helpers defined at the top of large test files (testStatus200, testErrorResponse, performOversizedRequest) and tagged with t.Helper()

Fixtures#

.github/testdata2/ and .github/testdata3/ contain Go template fixtures (*.tmpl) used by view-engine tests. No testdata/ directory at the repo root. No generated test fixtures or embedded test assets beyond templates.

Test patterns#

Table-driven tests#

  • Prevalence: Heavy — 293 hits of testCases/tt.Run/tc.name patterns across test files
  • Style: Anonymous struct slices; each struct has a name string field plus inputs and expected outputs. Sub-tests run via t.Run(tt.name, func(t *testing.T) { ... }).
  • Dedicated fixture file: path_testcases_test.go separates the router-path test matrix (hundreds of URL/param/match combinations) from test logic in path_test.go. This prevents large test files from becoming hard to navigate.
  • Example: bind_test.go:69Test_BindError_ErrorFormat uses t.Run("with field", ...) and t.Run("without field", ...) sub-tests with t.Parallel() inside each.

app.Test() — Fiber’s killer test primitive#

The most architecturally significant testing feature. app.Test(req *http.Request, config ...TestConfig) (app.go:1199) works by:

  1. Dumping the *http.Request to raw bytes via httputil.DumpRequest
  2. Writing those bytes into a testConn (an in-memory net.Conn backed by two bytes.Buffers)
  3. Serving the fake connection through the real fasthttp server pipeline
  4. Parsing the response bytes back into *http.Response

This lets test code use the standard net/http/httptest.NewRequest API while exercising the full fasthttp request/response cycle — no test server port needed, no goroutine leak risk, deterministic. It accounts for 1,875 usages across test files — essentially every unit test in every middleware package uses this pattern.

// Typical middleware test
app := fiber.New()
app.Use(New(Config{AllowOrigins: []string{"https://example.com"}}))
app.Get("/", func(c fiber.Ctx) error { return c.SendStatus(200) })

req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
req.Header.Set("Origin", "https://example.com")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)

Mocking approach#

  • Strategy: No mocking framework. Dependencies are small, concrete, and passed directly. The fasthttp RequestCtx is the main “seam” — tests construct one directly (&fasthttp.RequestCtx{}) and acquire a Ctx with app.AcquireCtx(...).
  • Interface fakes: app_test.go defines concrete fileView struct implementing the Views interface to test template rendering without a real filesystem renderer.
  • Custom context fakes: app_integration_test.go:35 defines integrationCustomCtx embedding *fiber.DefaultCtx to test the custom-context dispatch path (nextCustom vs next in the router).

Integration tests#

  • Present: Yes — app_integration_test.go (package fiber_test)
  • How: Uses fasthttputil.NewInmemoryListener() to bind the server to an in-memory socket, then exercises it with a real fasthttp.Client dialing the in-memory listener. The server lifecycle is managed via t.Cleanup (shutdown + error drain).
  • Scope: Tests middleware combination scenarios — how CORS, CSRF, session, cache, basicauth, etc. behave together when the error handler fires. A large middlewareCombinationTestCase table (with name, setup, configureRequest, handler, assertions, expectedStatus fields) drives each scenario.
  • Separation: A single dedicated file named app_integration_test.go in the root package. No build tags — runs in the default go test ./... invocation.

Fuzz tests#

  • Present: 1 fuzz target — FuzzUtilsGetOffer in helpers_fuzz_test.go
  • Guarded by build constraint: //go:build go1.18
  • Target: The content-negotiation getOffer() function — a parsing-heavy function that’s a natural candidate for fuzz-driven crashes.

Benchmarks#

  • 348 benchmark functions scattered across core and middleware test files
  • A dedicated client/request_bench_test.go focuses on HTTP client throughput
  • All benchmarks are co-located with the package they measure (no separate bench/ directory)

CI configuration#

From .github/workflows/test.yml:

jobs:
  unit:
    strategy:
      matrix:
        go-version: [1.25.x, 1.26.x]
        platform: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - run: go run gotest.tools/gotestsum@latest -f testname --
                ./... -race -count=1 -coverprofile=coverage.txt
                       -covermode=atomic -shuffle=on

  repeated:
    runs-on: ubuntu-latest
    steps:
      - run: go run gotest.tools/gotestsum@latest -f testname --
                ./... -race -count=15 -shuffle=on

Key CI choices:

  • -race on every run — data race detection is not optional
  • -shuffle=on — randomizes test execution order to surface order dependencies
  • -count=15 in the repeated job — runs the full suite 15 times to catch flaky tests (a rare CI discipline)
  • gotestsum for human-readable output
  • 6-platform matrix (2 Go versions × 3 OS)
  • Codecov coverage tracking with threshold: 0.5% regression gate; generated files (*_gen.go, *_msgp.go) explicitly excluded

Test quality observations#

What’s done well#

  • app.Test() is a best-in-class test primitive. It eliminates the need for a port, provides deterministic behavior, and speaks *http.Request/*http.Response so test helpers from the stdlib work. Nearly every middleware author in the Go ecosystem deals with this problem ad hoc; Fiber solved it structurally.
  • Parallel by default. 2,119 t.Parallel() calls — the team treats parallel tests as the norm, not an optimization. Combined with -race in CI, this surfaces data races during development.
  • -count=15 repeated job is an unusually high bar for flakiness detection. Most projects use -count=1 or -count=3. Running 15 iterations per CI push signals that the team has been burned by flaky tests and responded with infrastructure rather than acceptance.
  • Dedicated fixture file for large data tables (path_testcases_test.go) keeps test logic readable without sacrificing coverage breadth.
  • t.Helper() discipline — helper functions are consistently marked so failure output points to the call site, not the helper.
  • Integration test separation by file name and package suffix (fiber_test) rather than build tags — a pragmatic choice that keeps the integration tests part of the default run without adding tag ceremony.

What could improve#

  • Single fuzz target. The content-negotiation and routing code are complex string-parsing systems with security implications. A handful of additional fuzz targets (route matching, header parsing, cookie parsing) would be warranted given the project’s scale and HTTP-security exposure.
  • No testdata/ directory. Template fixtures live in .github/testdata2/ and .github/testdata3/, which is an unusual location that breaks the convention readers expect.
  • Coverage ignores internal/ (codecov.yml) — the internal storage packages have no stated coverage target.

Patterns worth emulating (for the book)#

  1. app.Test() pattern — building a zero-dependency, in-process test helper directly into the framework is something every Go library could offer. Shows how net.Conn and httputil.DumpRequest can replace a live server for unit tests.
  2. -count=15 flakiness job — separating “does it work” (count=1, 6-platform matrix) from “is it stable” (count=15, single platform) is a mature CI decomposition strategy.
  3. Table-driven test data in its own filepath_testcases_test.go demonstrates that when test data grows beyond ~200 lines, externalizing it to a dedicated file (still package fiber, not JSON/YAML) keeps both the data and the test logic readable.
  4. t.Parallel() everywhere + -shuffle=on — the combination forces correct test isolation from day one and makes flakiness visible at PR time rather than production.