Unit tests — *_test.go files alongside source in the same package (white-box, same package foo)
Integration tests — caddytest/integration/ as a separate package integration, black-box, testing a live server
Helper packages:
caddytest/ — a full integration test harness (see below)
internal/testmocks/ — stub Caddy modules registered via init(), imported as blank imports by integration tests that need module types not otherwise pulled in
Fixtures:
caddytest/integration/caddyfile_adapt/ — 218 .caddyfiletest plain-text files, each containing a Caddyfile snippet and the expected JSON output separated by ----------
caddyconfig/caddyfile/testdata/, caddyconfig/httpcaddyfile/testdata/, modules/caddyhttp/fileserver/testdata/ — static files for parser and fileserver tests
TLS test certificates checked into caddytest/ (.crt, .key, .pem files)
Prevalence: The dominant pattern for Caddyfile-to-JSON adapter tests
Style: Each .caddyfiletest file is its own subtest: the file’s name becomes the t.Run label; the file body is split on ---------- into input (Caddyfile) and expected output (JSON or error string). 218 fixtures.
Example:caddytest/integration/caddyfile_adapt_test.go:17 — TestCaddyfileAdaptToJSON reads all .caddyfiletest files and runs each as t.Run(filename, ...), calling caddytest.CompareAdapt with a unified diff on failure
Assessment: Excellent pattern for a DSL parser — each fixture is a self-documenting specification. Adding a new adapter test requires only a text file, no Go code.
Strategy: Stub Caddy modules implemented as real module implementations registered in test-only packages
Mechanism:internal/testmocks/dummyverifier.go defines a dummyVerifier that satisfies several interfaces (caddy.Module, caddytls.ClientCertificateVerifier, caddyfile.Unmarshaler) and registers itself via init(). Integration test packages blank-import _ "github.com/caddyserver/caddy/v2/internal/testmocks".
No generated mocks: No gomock/mockery usage. Fakes are hand-written and small.
Assessment: Appropriate for Caddy’s architecture — since all modules are interface-based and registered by init(), you inject a fake module the same way you’d inject a real one. Clean, but requires that fake modules be complete enough to satisfy compile-time interface checks.
How: In-process server startup. caddytest.NewTester(t) creates an HTTP client wired to 127.0.0.1. tc.InitServer(rawConfig, "caddyfile"|"json") POSTs the config to the admin API (localhost:2999/load), then polls GET /config/ with reflect.DeepEqual to confirm the config went live (up to 10 retries, 1-second sleep each). If no Caddy admin is already running, validateTestPrerequisites spawns one in a goroutine via caddycmd.Main().
Separation: Integration tests are gated by testing.Short() — tc.initServer calls t.SkipNow() if -short is active. CI runs with -short -race, so integration tests are skipped in the main pipeline and are only exercised on the s390x runner (which runs go test -p 1 -v ./... without -short).
Example:caddytest/integration/caddyfile_test.go:11 — TestRespond configures a file-serving Caddy via Caddyfile text, then AssertGetResponse checks the response body is "hello from localhost".
Assessment: Good coverage of all pure-parsing code paths (the highest-risk surface for malformed input). The choice of gofuzz predates Go 1.18 native fuzzing; migrating to func FuzzX(f *testing.F) would allow go test -fuzz in CI without a separate harness.
caddytest harness is a genuine asset. The in-process server pattern means integration tests exercise the real config lifecycle (JSON unmarshal, Provision, Validate, Start) with no mocking at the framework level. Any regression in module wiring shows up immediately.
File-driven adapter tests are a model pattern. 218 .caddyfiletest fixtures make the Caddyfile→JSON adapter nearly fully specified in human-readable diff-able text. New directives get test coverage by adding one file.
Race detector in CI.go test -short -race on every push catches concurrent access bugs at unit-test granularity without requiring integration tests.
Compile-time interface checks extend to test mocks.internal/testmocks has var _ caddytls.ClientCertificateVerifier = dummyVerifier{} — fakes are verified by the compiler.
Multi-platform CI (linux/mac/windows) — tests run on all three platforms, catching path separator issues (the adapter test explicitly handles filepath.Separator).
Integration tests skipped in main CI. The -short flag in the primary ci.yml job means the 218 integration tests (and all caddytest/integration/ tests) never run on Linux/Mac/Windows PRs — only on s390x. A PR can break an integration test without any CI signal on standard platforms.
Retry-based config polling is fragile.ensureConfigRunning polls every 1 second for up to 10 seconds. A heavily loaded CI runner could flake. A channel-based or webhook notification from the server would be more deterministic.
Testify is almost absent but not fully. Two files use testify/assert and testify/require while the rest use stdlib assertions. The inconsistency is minor but adds a dependency that could be eliminated.
Legacy gofuzz vs native fuzzing. The 7 fuzz targets use the old dvyukov/go-fuzz API behind a build tag. Migrating to testing.F would allow running fuzz tests in CI with go test -fuzz and better tooling support.
No benchmark suite visible. For a performance-sensitive HTTP server, there are no func Benchmark* functions in the scanned files — a gap for regression testing of hot paths (routing, header handling, TLS).
File-driven fixture tests for DSL adapters — the .caddyfiletest split-file approach is clean, readable, and trivially extensible. Useful for any project with a config language or serialization format.
In-process server integration harness — starting the real binary in a goroutine and driving it via its own API is more faithful than mocks and avoids the complexity of Docker/testcontainers for integration tests.
Stub modules registered via init() in a test-only package — zero reflection, compiler-verified, and reuses the same registration mechanism as production code.