NATS Server — Testing#
Test metrics#
- Test files: 127 (
_test.gofiles, excluding vendor) - Total Go files: 251
- Ratio (test files / source files): ~50% — exceptionally high for a project of this complexity
- Test functions: 3,141
Test*functions; 313Bench*functions; 3Fuzz*functions - Test frameworks: Pure stdlib
testingpackage only — no testify, gomock, ginkgo, or any third-party assertion library
Test organization#
Placement#
Tests are colocated with source in the same package (white-box / internal testing). The server package tests declare package server, giving direct access to unexported symbols. The external test/ directory provides a separate package (package test) for black-box protocol-level tests that interact over raw TCP.
Helper packages#
test/test.go(660 lines,package test): Classic protocol-level test scaffold for the original NATS wire protocol. ProvidesRunServer,RunServerWithConfig,sendFun/expectFunfunction types for sending raw protocol commands and matching regex responses overnet.Conn. Functions likecheckMsg,expectNothing,doConnect,setupRoutelet tests interact at the byte level without a NATS client library. Used by the test files intest/(cluster, gateway, leafnode black-box tests).server/test_test.go(package server): Defines the customrequire_*assertion family —require_True,require_False,require_NoError,require_NotNil[T],require_Contains,require_NoPanic, and more (all generic or variadic where appropriate). Also definesDefaultTestOptions,RunServer,RunRandClientPortServer. Every assertion integrates with the Antithesis SDK (see below).server/jetstream_helpers_test.go(2,236 lines,package server): The most significant test infrastructure file in the codebase. Definesclusterandsuperclustertypes that spin up multi-server in-process clusters with full JetStream, routing, and gateway topology. Provides dozens of helper methods:waitOnLeader,waitOnStreamLeader,waitOnConsumerLeader,waitOnPeerCount,createJetStreamCluster,createJetStreamSuperCluster,createMixedModeCluster, and so on. Itsinit()tunes Raft timing constants (heartbeat interval, election timeouts) to speed up leader elections in test scenarios. This file is the foundation for nearly all JetStream cluster tests.internal/antithesis/: A thin wrapper around the Antithesis SDK. When built without theenable_antithesis_sdktag, all assertion functions are NOOPs. When built with the tag (on the Antithesis platform), they fire fault-injection assertions. Used in test helpers to annotate timeout conditions and invariant violations so the platform can trigger targeted inputs.internal/testhelper/:DummyLogger(async.Mutex-protected logger capture type) used in tests that verify log output.
Fixtures#
test/configs/: NATS.conffiles, TLS certificates, NKey files, and JWT-related fixtures used by the black-box test suite. No testdata directories elsewhere — fixtures are config files and in-memory constructed strings.- No embedded fixtures (
//go:embed) or generated test data. Config strings for JetStream cluster templates are defined as Go string literals injetstream_helpers_test.go.
Test patterns#
Table-driven tests#
- Prevalence: Occasional — 49 occurrences of table-driven patterns (
tests := [],testCases,tt.Run,tc.name) - Style: Anonymous struct slice with
t.Runsubtests for parametric cases; most tests are single-scenario integration tests rather than table-driven unit tests - Example:
server/subject_transform_test.gouses a struct slice withinput/expectedfields to test subject transforms across many combinations
Mocking approach#
- Strategy: No mocking whatsoever. Tests use real in-process servers exclusively. Dependencies are not mocked — instead, a real NATS server is started with
RunServer(opts)(which callsNewServerands.Start()in a goroutine) and torn down indefer s.Shutdown()ordefer c.shutdown(). - Example: A JetStream cluster test calls
c := createJetStreamClusterExplicit(t, "R3S", 3)which starts three real servers bound to random ports, forms a cluster, waits for readiness, and returns theclusterhandle. Tear-down isdefer c.shutdown(). - Consequence: All tests are integration tests by nature. There is no unit test isolation below the server level — no stub transports, no mock accounts. This is a deliberate philosophy: NATS’s correctness lies in its distributed behavior, which cannot be exercised with mocks.
Integration tests#
- Present: Yes — effectively all
server/tests are integration tests (spin up real servers) - How: In-process servers bound to
127.0.0.1withPort: -1(OS-assigned random port). Cluster tests useCluster.Port: -1as well to avoid conflicts. No Docker, no testcontainers, no external processes — the full server runs in-process in the test binary. - Separation by build tag: Tests are partitioned into named subsets using Go build tags and function naming conventions, managed by
scripts/runTestsOnTravis.sh:TestJetStream*— non-clustered JetStream tests (taggedskip_js_cluster_tests, etc.)TestJetStreamCluster*— split across 4 CI jobs usingskip_js_cluster_tests_1/2/3/4tagsTestJetStreamSuperCluster*— gateway-bridged multi-clusterTestNRG*— Raft group tests (run inraft_testsjob)TestMQTT*,TestMsgTrace*,TestJWT*— dedicated CI jobsTestNoRace*innorace_1_test.goandnorace_2_test.go— tagged!race && !skip_no_race_[12]_tests— run without-raceflag because they exercise timing-sensitive or throughput paths where the race detector overhead would cause false failures or extreme slowdowns
- Race detection: CI conditionally applies
-racebased on branch: PRs and feature branches run with-race; main/release branches do not (presumably for throughput). TheRACEenv variable is set in the workflow and passed togo test.
Fuzz tests#
Three Fuzz* functions using Go 1.18+ native fuzzing:
FuzzParser(server/parser_fuzz_test.go): Exercises the NATS protocol parser with arbitrary byte sequences; the corpus includes valid NATS commands to guide mutationFuzzServerTLS(server/server_fuzz_test.go): Fuzzes TLS handshake handlingFuzzSubjectsCollide(server/subject_fuzz_test.go): Fuzzes theSubjectsCollidefunction with pairs of subject strings, seeded from known collision/non-collision pairs
Benchmarks#
313 benchmark functions, concentrated in:
test/bench_test.goandtest/fanout_test.go: Classic pub/sub throughput benchmarks (fanout to N subscribers, queue groups, request-reply latency)server/mqtt_ex_bench_test.go: MQTT-specific throughputserver/store_test.goand related: FileStore and MemStore write/read throughput
CI architecture#
GitHub Actions (tests.yaml) runs ~15 parallel jobs, each with a 30-minute timeout:
store,js-no-cluster,raft,js-consumers,js-cluster-1/2/3/4,js-supercluster,no-race-1/2,mqtt,msgtrace,jwt,server-pkg-non-js,non-server-pkg- All test jobs depend on
build-latest,build-supported, andlint - Separate
nightly.yamlandlong-tests.yamlworkflows for extended runs cov.yamlfor coverage reporting
The naming convention (TestJetStreamCluster_, build tags for splitting) is a hand-crafted test-sharding scheme that predates native Go test sharding — a pragmatic solution for a 3,000-test suite where JetStream cluster tests dominate runtime.
Test quality observations#
What’s done well#
- In-process cluster infrastructure (
jetstream_helpers_test.go) is the standout achievement. Theclusterandsuperclusterhelpers allow writing distributed correctness tests (leader election, stream replication, snapshot/restore) in ~10 lines of test code. This infrastructure is arguably as complex as many standalone projects. checkForpolling helper (server/server_test.go:57) — a universal retry loopcheckFor(t, totalWait, sleepDur, func() error)— eliminates flakytime.Sleepcalls. Distributed tests poll state with this pattern rather than asserting synchronously.- No third-party test libraries — pure stdlib means no dependency churn and predictable behavior. The hand-rolled
require_*family (with generics forrequire_NotNil[T]) covers all needed assertions without importing testify. - Antithesis integration is unusually sophisticated. Test assertions are wired to a fault-injection platform that can replay specific executions with targeted perturbations. The
internal/antithesispackage cleanly decouples this from normal test runs (NOOP by default). - Fuzz test corpus quality:
FuzzSubjectsCollideseeds with semantically meaningful cases (known collision pairs) rather than random bytes, dramatically improving fuzzer efficiency. !racepartitioning is explicit and documented:norace_1_test.go/norace_2_test.gocarry build tag comments explaining why-raceis excluded. Tests are not silently skipping race detection.- Raft timing tuning in
init()(jetstream_helpers_test.go:53-63): ReducinghbIntervalfrom production values to 50ms and capping election timeouts dramatically speeds up Raft leader election in tests without changing production behavior.
What could improve#
- t.Parallel() adoption is minimal (22 uses). Given that each test starts servers on random ports, many tests could safely parallelize. The current approach relies on CI job sharding instead of in-process parallelism, which is coarser.
- Table-driven tests are underused in the
server/package. Many tests that exercise parametric behavior (e.g., config option combinations) repeat boilerplate start/stop sequences rather than using subtests. Thetest/package is better here. - No
t.Cleanupfor server shutdown — tests usedefer s.Shutdown()ordefer c.shutdown(). While functionally equivalent,t.Cleanupwould run cleanup before subtests exit and would be more consistent with modern Go test style. jetstream_helpers_test.gois a single 2,236-line file with no internal organization. As a shared dependency for ~70 test files, it has become a catch-all. Splitting by topology type (single-cluster, super-cluster, leaf-node, gateway proxy) would improve navigability.
Patterns worth emulating#
- The
checkForpolling idiom is a generalizable pattern for any asynchronous or eventually-consistent system test. It keeps tests from sleeping fixed durations and makes timeout failures self-describing. - In-process cluster construction as a test strategy — starting real servers rather than mocking — eliminates an entire class of mock-drift bugs where the mock’s behavior diverges from the real implementation over time. For infrastructure software with complex distributed protocols, this is often the right trade-off despite higher setup cost.
- Build-tag test sharding by naming convention (prefixes + tags) is a workable pattern for large test suites before native sharding is available. The explicit script (
runTestsOnTravis.sh) documents the partition logic clearly. - Antithesis-aware assertions — wrapping test assertions with platform-specific hooks (that are NOOPs in normal runs) allows plugging a distributed fault-injection platform in later without modifying test logic. This is a forward-compatible pattern for mission-critical systems.