Prometheus — Testing#
Test metrics#
- Test files: 259
- Source files (non-test): 438
- Ratio (test files / source files): ~0.59 — solid coverage for a project of this complexity
- Test frameworks:
testify/require(primary assertion library),google/go-cmp(deep equality with custom comparers),prom_testutilfromprometheus/client_golang(metrics assertion),go.uber.org/goleak(goroutine leak detection),testing/synctest(Go 1.24 deterministic goroutine scheduling) - Benchmark functions: 133
- Fuzz functions: 8
Test organization#
- Placement: Overwhelmingly same-package (247 files use
package <pkg>, only 10 usepackage <pkg>_test). Tests get full access to unexported symbols, which the team clearly prefers over strict black-box testing. - Helper packages:
util/testutil— a curated set of test utilities:cmp.go:RequireEqual/RequireEqualWithOptionswrappinggo-cmpwith a customlabels.Equalcomparer, solabels.Labelsare compared by content rather than Go struct equality.testing.go:TolerantVerifyLeak(m *testing.M)— wrapsgoleak.VerifyTestMainwith pre-configured ignores for known false-positive goroutines from opencensus, k8s klog, and client-go workqueue.context.go:MockContextandMockContextErrAfter— simple stub implementations ofcontext.Contextfor testing cancellation paths without timers.directory.go,port.go,roundtrip.go: utility helpers for temp dirs, free port allocation, and HTTP round-trip testing.synctest/: thin wrapper around Go 1.24’stesting/synctestpackage, providingTest(t, f)andWait()for deterministic concurrency tests.
util/teststorage— a real TSDB-backedTestStoragethat sets up an in-process Prometheus TSDB with relaxed block/retention windows. Registered viat.Cleanupso callers don’t need to close it. Used by scrape, PromQL, and rules tests to write and query real time-series data.util/fuzzing— a dedicated package containing allFuzzXxxfunctions (8 total), covering: text format parsing, OpenMetrics parsing, metric selector parsing, PromQL expression parsing, XOR chunk encoding/decoding (two variants), protobuf parsing.promql/promqltest— a full DSL-based test engine (described in detail below).
- Fixtures: Each major package has a
testdata/directory with YAML, text, or binary fixtures:config/testdata/(config files),scrape/testdata/(target configs),model/textparse/testdata/(wire format samples),promql/promqltest/testdata/(DSL test scripts),tsdb/testdata/,web/api/v1/testdata/(HTTP response fixtures), and others.
Test patterns#
Table-driven tests#
- Prevalence: Heavy — 382 occurrences of table-driven test patterns across
*_test.gofiles. - Style: Anonymous struct slice, e.g.,
tests := []struct{ name string; input ...; expected ... }{}followed byfor _, tc := range tests { t.Run(tc.name, ...) }. Named structs are rare; anonymous inlined structs dominate. - Example:
model/relabel/relabel_test.go:30—TestRelabeluses an anonymous struct withinputLabels,cfg []*config.RelabelConfig,outputLabels, etc.; 20+ cases covering all relabeling rules in a single function. - Benchmark tables: Also used for benchmarks —
model/relabel/relabel_test.go:1069usesb.Run(tt.name, ...)for parametric benchmarks across label set sizes.
DSL-driven PromQL testing (promqltest)#
- What it is: A custom domain-specific language for declaratively specifying PromQL evaluation tests. Lives in
promql/promqltest/testdata/*.testfiles. - Format:
Commands:load 5m http_requests{job="api-server", instance="0"} 0+10x10 eval instant at 50m sum by (group) (http_requests) {group="production"} 300load,eval instant,eval range,clear. Modifiers:eval_fail,eval_warn,eval_ordered. Theexpectdirective can assert on warning/info annotations by regex. - Why it matters: The test engine (
promqltest/test.go) uses//go:embedto bundle all.testfiles into the binary. Any PromQL engine implementing thepromql.QueryEngineinterface can be run against this suite — the test corpus is the authoritative specification for PromQL semantics. Third-party PromQL implementations (e.g., Thanos, Cortex) import and run this suite. - Scale: Testdata files cover:
aggregators,at_modifier,collision,duration_expression,extended_vectors,fill-modifier,functions,histograms,info,limit, and more. Hundreds of eval assertions.
Mocking approach#
- Strategy: No gomock or mockery. Prometheus uses two strategies:
- Real in-process implementations: Most tests use
util/teststorage.New(t)to get a real TSDB,httptest.NewServerfor HTTP handlers, and real discovery managers. This makes tests more realistic but heavier. - Manual fakes via interfaces: Storage interfaces (
storage.Appender,storage.QueryableFunc, etc.) are satisfied by small hand-written fakes in test files. For example,scrape_test.gocontainsnopAppender,mutatingAppender, and other local structs that implementstorage.Appenderwith custom behavior.
- Real in-process implementations: Most tests use
- Notable:
util/testutil.MockContextprovides a lightweight context stub for testing cancellation without real timers — lighter than full context withcontext.WithCancel. prom_testutil: Tests of metrics-emitting components usegithub.com/prometheus/client_golang/prometheus/testutilto assert on Prometheus metric values:testutil.ToFloat64(counter),testutil.CollectAndCompare(reg, expectedMetricsReader). Used 209 times across test files.httptest: 201 occurrences ofhttptest.NewServer/httptest.NewRecorder— the standard Go HTTP test server is heavily used for testing the scrape, remote write, and web API layers.
Goroutine leak detection (goleak)#
- Present: Yes —
goleakis used in 30 places across test packages. - Pattern:
TestMainfunctions calltestutil.TolerantVerifyLeak(m)(which wrapsgoleak.VerifyTestMain) at the package level. Individual tests may also calldefer goleak.VerifyNone(t)for targeted checks (e.g.,scrape_test.go:1272). - Known false positives: The
TolerantVerifyLeakwrapper ignores goroutines from opencensus, k8s klog, and client-go workqueue that are background daemons started by imported libraries and cannot be stopped per-test. - Significance: Goroutine leak detection is rare in Go projects of this size. Its presence reflects the project’s awareness that scrape loops, discovery managers, and notification senders are prone to goroutine leaks if shutdown paths are incorrect.
Deterministic concurrency testing (synctest)#
- Present: Yes — the notifier package uses
util/testutil/synctest(wrapping Go 1.24’stesting/synctest). - Example:
notifier/manager_test.go:704— tests for alert send retry logic usesynctest.Test(t, func(t *testing.T) { ... })so thattime.Sleepcalls inside the notifier’s retry loop use fake time.synctest.Wait()advances fake time until all goroutines are blocked. - Why notable: This is a very new Go testing facility (Go 1.24). Prometheus is an early adopter, likely because testing retry/backoff logic in the notifier requires precise control over elapsed time without real sleeps.
Integration tests#
- Dedicated integration test files: None found (
*_integration_test.go,*_e2e_test.go— zero matches). - How integration is achieved: Instead of separate files, Prometheus uses:
- Test flags:
TestVersionUpgradeincmd/prometheus/main_upgrade_test.gois gated by--test.version-upgrade=true(a customflag.Bool). It runs in a dedicated CI job that downloads historical Prometheus release binaries and exercises TSDB upgrade/downgrade paths. - TSDB isolation flag: CI runs
go test ./tsdb/ -test.tsdb-isolation=falseas a separate step — a custom test flag exposed by the TSDB package to toggle transaction isolation behavior, exercising a different code path. - Multi-arch testing: CI runs
GOARCH=386 go test ./...to catch 32-bit arithmetic bugs. - Build tag matrix: CI runs
go test --tags=dedupelabels ./...,--tags=slicelabels,--tags=forcedirectio— three different label storage implementations and a direct-IO TSDB path are each tested in separate CI jobs.
- Test flags:
CI structure#
Five distinct Go test jobs in .github/workflows/ci.yml:
test_go:make GO_ONLY=1(standardgo test ./...) + TSDB isolation variant.test_go_more: Three build-tag variants (dedupelabels,slicelabels+race,forcedirectio+race) + GOARCH=386 + proto regeneration check.test_version_upgrade: Version upgrade/downgrade test with real binary downloads.test_go_oldest: Runs the full suite against Go N-1 to catch version regressions.fuzzing: Matrix job running eachFuzzXxxfunction for 4 minutes, triggered on schedule.
Test quality observations#
What’s done well#
- Goroutine leak detection via
goleakin 30 packages — rare discipline for a project this large, directly connected to the concurrency-heavy scrape and discovery layers. - Real storage in tests —
util/teststoragegives tests a real TSDB, making scrape and PromQL tests high-fidelity. Bugs that would slip through a mock appender are caught. promqltestDSL — a genuinely innovative testing technique. The declarative format is human-readable, version-controllable, and reusable across third-party PromQL implementations. It is the authoritative PromQL specification.- Build-tag matrix — testing three separate label storage implementations (dedupelabels, slicelabels, default) and a direct-IO path ensures that experimental optimizations don’t silently break semantics.
- 133 benchmarks — performance is first-class. The AGENTS.md contributor guide requires benchmarks with
benchstatoutput for any performance PR. - 8 fuzz targets — covering the parsing hot paths (text format, OpenMetrics, PromQL, XOR chunks, protobuf) where malformed input from scrape targets could crash the server.
go-cmpwith custom label comparer —labels.Labelshas non-obvious equality semantics (different underlying representations can be logically equal). TheRequireEqualwrapper enforces correct comparison without burdening every test author.- Early adoption of
testing/synctest— the notifier retry tests benefit from deterministic time control, eliminating flakytime.Sleep-based assertions.
What could improve#
- Coverage is uneven: The scrape and PromQL packages are extensively tested; some discovery providers have thin test coverage (particularly newer cloud providers added recently).
- Same-package bias: The heavy preference for same-package tests (247 vs 10 external) means the public API surface is less systematically tested. A consumer’s perspective on API ergonomics could be missed.
- No end-to-end HTTP test suite: There is no self-contained integration test that boots a real Prometheus server, scrapes a target, evaluates a query, and asserts on results. The closest is
TestVersionUpgrade, which is gated behind a flag and run in a dedicated CI job. goleakcoverage gaps: Only 30 of 259 test packages have goroutine leak detection. Given the concurrency model, broader coverage would be valuable.
Patterns worth emulating#
promqltestDSL: Any project with a complex query or evaluation engine should consider a declarative test DSL. The cost of building the parser is paid once; the benefit is hundreds of readable, maintainable test cases.util/teststorage: The pattern of aNew(t testing.TB, opts ...Option) *TestStoragehelper that usest.Cleanupfor teardown is widely applicable. It gives every test a real storage backend with zero boilerplate.TolerantVerifyLeakwith documented false-positive exceptions: The explicit comment-annotated ignore list intestutil.TolerantVerifyLeakis instructive — it names the upstream issue links, making it clear that these ignores are not laziness but documented limitations of third-party libraries.- Build-tag matrix for compile-time configuration: When a project has compile-time-selected implementations (via build tags), exercising each in CI is the correct approach. Prometheus makes this visible as separate CI jobs rather than hidden in a single script.