Placement: Almost exclusively same-package tests (254 of 255 test files use the package’s own name, not a _test suffix). The one external _test package is the rare exception, not the rule.
Helper packages:
pkg/testhelpers/ — functional-option config builders (BuildConfiguration, WithRouters, WithServices, WithMiddlewares, etc.) that compose dynamic.HTTPConfiguration objects for unit tests. These use the same functional-options idiom documented in the patterns analysis.
integration/try/ — a polling/retry helper with typed ResponseCondition functions (StatusCodeIs, BodyContains, BodyNotContains, etc.) and CITimeoutMultiplier support for slower CI environments. Wraps HTTP polling behind try.GetRequest / try.Request / try.Do.
Fixtures:testdata/ directories under pkg/api/ and pkg/redactor/ contain JSON snapshots of expected API responses. Tests load these files and compare against actual output. The --update_expected flag (via flag.Bool("update_expected", false, ...)) allows regenerating golden files in-place — a standard Go golden file pattern.
Prevalence: Heavy — 770 occurrences of testCases, tt.Run, tc.name, tc.expected across test files.
Style: Anonymous struct slice is the default. Struct fields are desc (description) + inputs + expected outputs. Named struct types appear occasionally in larger test suites.
Example:pkg/middlewares/ratelimiter/rate_limiter_test.go — TestNewRateLimiter defines a testCases []struct{ desc string; config dynamic.RateLimit; expectedMaxDelay time.Duration; ... } slice covering a dozen scenarios. pkg/server/configurationwatcher_test.go uses the same pattern for config-reload scenarios.
Strategy: Manual inline mock structs implementing the relevant interface. No gomock, mockery, or other code-generation tool is used anywhere.
Example:pkg/server/configurationwatcher_test.go defines mockProvider as a local struct that satisfies the Provider interface by implementing Provide(), ThrottleDuration(), and Init(). The struct holds pre-canned messages []dynamic.Message to replay. This pattern repeats throughout: each test file defines its own minimal mock for exactly the interfaces it needs, keeping mocks co-located with their tests.
Prevalence: Heavy — 417 occurrences of httptest.NewRecorder or httptest.NewServer.
Pattern: Middleware tests follow a standard shape: create an httptest.ResponseRecorder, wrap a handler under test with the middleware, fire an http.Request, and assert on the recorded response. The pkg/testhelpers config builders keep test setup concise.
Usage: Moderate — 324 occurrences of t.Parallel(). Unit tests in hotly-contested packages (middlewares, providers) are parallelized; integration tests are not.
Present: Yes — 39 test files in integration/, covering Docker, Consul, etcd, Redis, k8s (k3s), ACME, gRPC, WebSocket, TCP, UDP, TLS, tracing, rate limiting, headers, routing, and more.
How:testcontainers-go manages Docker containers programmatically. The BaseSuite struct (embedding testify/suite.Suite) handles Docker network lifecycle, container creation from YAML compose fixtures, container teardown, and log capture on failure. The actual Traefik binary is launched as a subprocess (exec.Command) pointing at config fixtures in integration/fixtures/. Tests poll http://127.0.0.1:8080/api/rawdata via try.Request to confirm Traefik is ready before asserting on routing behavior.
Separation: All integration tests live in the top-level integration/ directory. No build tags are used; separation is purely by directory. Each feature domain gets its own _test.go file (docker_test.go, consul_test.go, redis_test.go, etc.), each declaring its own Suite struct and TestXxxSuite(t *testing.T) entry point.
Conformance tests: Two additional conformance-level test suites exist: integration/gateway_api_conformance_test.go (Gateway API) and integration/knative_conformance_test.go, each driven by separate CI workflows.
Coverage breadth: The integration suite spans nearly every provider and protocol Traefik supports. Testing the real binary against real containers (via testcontainers-go) rather than mocking the runtime gives high confidence that the full system works.
Testhelper ergonomics:pkg/testhelpers provides a clean DSL for building complex dynamic.Configuration objects in unit tests, avoiding repetitive struct literal boilerplate. The functional-option builder makes test intentions explicit.
Golden file pattern: The --update_expected flag in API tests allows regenerating JSON fixtures with a single command, keeping expected output under version control without manual editing. This is a maintainability win for serialization-heavy tests.
try package design: The integration/try package encodes polling idioms (with exponential backoff, CI multiplier, and typed response conditions) as composable functions. This prevents ad-hoc time.Sleep loops in individual test files and centralizes retry semantics.
Table-driven discipline: 770 table-driven test cases across the codebase — the team has consistently resisted writing one-test-per-scenario functions, keeping test files concise and the full case matrix visible at a glance.
t.Parallel() adoption: 324 uses of t.Parallel() significantly reduce unit test suite wall-clock time without requiring external tooling.
Mock proliferation: Each test file defines its own ad-hoc mock structs. With 255 test files, there is significant duplication — several files define nearly identical mockProvider or mockHandler types. A shared pkg/testhelpers/mocks package would reduce boilerplate and keep mock contracts synchronized with interfaces.
Integration test locality: Integration tests run against a real Traefik binary (../dist/linux/amd64/traefik), which requires a prior build step. This creates friction for local development and can cause confusing failures when the binary is stale. An in-process integration layer (running the server in a goroutine) would be faster and eliminate the build dependency.
No fuzz tests: Given Traefik parses untrusted rule expressions (Host(...), PathPrefix(...), etc.) and provider configs, fuzz testing the rule parser and config loaders would meaningfully increase security and reliability confidence. None exists.
CITimeoutMultiplier as a smell: The explicit CITimeoutMultiplier = 3 in integration/try/try.go indicates timing-sensitive tests that could be made deterministic by polling until a condition is met rather than sleeping.
testify/suite for integration test lifecycle — The BaseSuite + named XxxSuite pattern cleanly handles SetupSuite/TearDownSuite/TearDownTest without global state. Each suite manages its own containers, so suites can run concurrently at the file level.
try package as a first-class test concern — Encapsulating polling/retry as composable ResponseCondition functions is a reusable pattern for any project that tests eventually-consistent state.
Functional-option config builders in testhelpers — Using the same functional-option idiom in test DSLs as in production code creates consistency and makes test setup readable without fighting Go’s verbose struct literals.
Golden file with -update flag — The flag.Bool("update_expected", false, ...) pattern for snapshot tests is well-understood, version-control-friendly, and trivially adaptable to any serialization format.