caddyserver/caddy
Testing
Caddy has three flavors of tests, and you will likely want to add to all three when writing a new feature:
- Unit tests — table-driven
*_test.gofiles alongside the code (modules/...,caddyconfig/...). - Integration tests — full Caddy instances spun up with
caddytest.NewTester(caddytest/integration/). - Caddyfile adapt tests — input/expected fixture pairs under
caddytest/integration/caddyfile_adapt/that compare adapted JSON against a golden file.
Run everything with race detection (matches CI):
go test -race -short ./...Table-driven unit tests
The preferred pattern, lifted from AGENTS.md:
func TestFeature(t *testing.T) {
for i, tc := range []struct {
input string
expected string
wantErr bool
}{
{input: "valid", expected: "result", wantErr: false},
{input: "invalid", expected: "", wantErr: true},
} {
actual, err := Function(tc.input)
if tc.wantErr && err == nil {
t.Errorf("Test %d: expected error but got none", i)
}
if !tc.wantErr && err != nil {
t.Errorf("Test %d: unexpected error: %v", i, err)
}
if actual != tc.expected {
t.Errorf("Test %d: expected %q, got %q", i, tc.expected, actual)
}
}
}Existing examples:
modules/caddyhttp/matchers_test.go(~30 KB) — exhaustive matcher table tests.caddyconfig/caddyfile/parse_test.go(~24 KB) — parser cases including malformed input.replacer_test.go— placeholder substitution.
Integration tests with caddytest.Tester
caddytest/caddytest.go provides a test harness that spins up a real Caddy instance against an isolated admin port and asserts HTTP behavior:
func TestHTTPFeature(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
}
localhost:9080 {
respond "hello"
}`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "hello")
}Conventions:
- Use the non-default ports
9080,9443,2999so a real Caddy on the host doesn't collide. - Tests are in
caddytest/integration/; run them withgo test ./caddytest/integration/.... - The
mockdns_test.gofile incaddytest/integration/registers a fake DNS provider (dns.providers.mock) for ACME-DNS-challenge tests.
InitServer accepts both caddyfile and json as the second argument, so the same harness works whether you want to test the adapter or the underlying JSON.
Caddyfile adapt fixtures
Under caddytest/integration/caddyfile_adapt/ and caddyconfig/caddyfile/testdata/ you will find pairs of input fragments and expected JSON outputs. The test runner reads each fixture, runs the Caddyfile adapter, and diffs the result against the golden file.
When adding a directive or changing how one expands, add a fixture pair so the change is visible in git diff and so future refactors can't silently regress the output.
Fuzz tests
Several core parsers ship fuzz harnesses:
caddyconfig/caddyfile/lexer_fuzz.go,formatter_fuzz.gocaddyconfig/httpcaddyfile/addresses_fuzz.goreplacer_fuzz.go,duration_fuzz.go,listeners_fuzz.gomodules/caddyhttp/templates/frontmatter_fuzz.go
Run with go test -fuzz=FuzzX -fuzztime=30s ./path/.... Add a corpus seed to testdata/fuzz/<TestName>/ for known-tricky inputs.
What CI runs
.github/workflows/ci.yml does:
go build ./cmd/caddywith-trimpath -ldflags="-w -s".- A "smoke test" via
./caddy start && ./caddy stopto make sure the service-style start/stop path works. go test -v -coverprofile=cover-profile.out -short -race ./....- The same matrix on Linux, macOS, and Windows.
- A separate
s390x-testjob that rsyncs the tree to an IBM Z VM and runs the suite there.
.github/workflows/lint.yml runs golangci-lint, and cross-build.yml confirms goreleaser can produce binaries for every advertised platform.
Related pages
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.