Nomad — Testing#
Test metrics#
- Test files: 814
*_test.gofiles - Total Go files: 2,129 (including test files)
- Ratio (test files / source files): ~62% (814 / ~1315 non-test source files) — very high
- Test frameworks:
github.com/shoenig/testandgithub.com/shoenig/test/must— primary assertion library in newer code (787 occurrences)github.com/stretchr/testify/requireandtestify/assert— legacy assertions, still widespreadgithub.com/shoenig/test/wait— async polling assertions- stdlib
testing— universal base - No gomock, ginkgo, or goconvey
Test organization#
- Placement: Both same-package (
package nomad) and_test-suffixed external packages (package nomad_test). The dominant style is same-package testing, giving tests access to unexported types — appropriate for testing internal state machines like the eval broker and scheduler. - Helper packages:
testutil/(top-level) —WaitForResult/Waitpolling helpers,TestMultiplier()for CI-aware timing,TestServer(fork/exec a real nomad binary), TLS cert generation, Vault test stubsnomad/testing.go— in-processTestServer(t, cb),TestACLServer(t, cb),TestConfigForServer(t)for spinning up a full Nomad server inside the test process; used by allnomad/package testsnomad/mock/— rich struct factory package:mock.Job(),mock.Alloc(),mock.Node(),mock.Eval(),mock.ACLManagementToken(), HCL snippets. Essentially a domain-object DSL for tests.api/internal/testutil/— separateNewTestServer(t, cb)that forks a real binary (avoiding the import cycle betweenapi/and the server packages)client/testutil/— Docker availability checks, driver-compatible skip helpers, RPC test utilitiesdrivers/mock/— fullDriverPluginimplementation used as a stand-in driver in integration testsclient/serviceregistration/mock/—ServiceRegistrationHandlerimplementing the service registration interface for unit testing task runnersplugins/csi/fake/— fake CSI plugin implementation for storage plugin testsclient/allocrunner/taskrunner/testing/— task runner test stubsinternal/testing/apitests/— API integration test helpers
- Fixtures:
testdata/directories incommand/,drivers/docker/,helper/tlsutil/,helper/snapshot/, andclient/state/hold HCL configs, TLS certs, job spec files, and BoltDB snapshots
Test patterns#
Table-driven tests#
- Prevalence: Heavy — 1,352 matches for
testCases,testCases :=,tt.Run,tc.nameacross test files - Style: Anonymous struct slice with a
namestring field and subtest invocation viat.Run(tc.name, ...). Named-struct style is also common for fingerprinter tests where expected output varies per environment. - Example:
client/fingerprint/network_test.go:285—testCases := []struct { name string; ... }withfor _, tc := range testCases { t.Run(tc.name, ...) };scheduler/feasible/feasible_test.go— inline struct slices for constraint evaluation permutations
ci.Parallel pattern#
- Prevalence: Extremely pervasive — 4,272 occurrences of
ci.Parallel(t)at the top of test functions - Mechanism:
ci/slow.gowrapst.Parallel()with a check for theCIenvironment variable. When running in CI (GitHub Actions, CircleCI), parallelism is suppressed in favor of serial execution with unrestrictedGOMAXPROCS. Locally, tests run in parallel as usual. - Slow test gating:
ci.SkipSlow(t, reason)skips tests unlessNOMAD_SLOW_TEST=1, separating expensive tests (full cluster bootstraps, multi-second waits) from the fast path - Assessment: A sophisticated and well-thought-out approach. CI environments get throughput from GOMAXPROCS; local development gets latency from parallelism. This is a pattern worth emulating.
Mocking approach#
- Strategy: Interface-based hand-rolled fakes — no code-generation tools like mockery or gomock. Each subsystem provides its own
mock/orfake/package with concrete implementations of the relevant interfaces. - Examples:
drivers/mock/—mock.Driverimplements the fulldrivers.DriverPlugininterface, configurable via task config HCL to simulate blocking starts, kill delays, crash behaviorsclient/serviceregistration/mock/— records allRegisterWorkload/DeregisterWorkloadcalls in a slice for assertion; supports injecting error responses via function fieldsplugins/csi/fake/— CSI plugin fake with configurable error injection for each RPCnomad/mock/— struct factories returning fully-populated domain objects, not behavioral mocks
- Philosophy: The codebase clearly prefers hand-written fakes over generated mocks. Fakes are full implementations that can be configured for test scenarios, which catches more real integration problems than stub-based mocking.
Integration tests#
- Present: Yes — three distinct levels
- Level 1 — In-process server:
nomad.TestServer(t, cb)creates a fully functional Nomad server (Raft, state store, RPC) inside the test process. Tests innomad/call real RPC handlers against a real in-memory state store. These are technically unit tests by build tooling but are functionally integration tests. - Level 2 — Fork/exec binary:
testutil.NewTestServer(t, cb)in the top-leveltestutil/andapi/internal/testutil/packages forks an actualnomadbinary as a subprocess, writes a temp config, and waits for it to become healthy. Used byapi/package tests to avoid the import cycle with server internals. Tears down viat.Cleanup. - Level 3 — e2e cluster:
e2e/directory contains 68TestXxxfunctions organized into ~35 feature subdirectories (acl, cni, connect, csi, deployment, docker, etc.). These require a running Nomad cluster (not spun up by the test itself).e2eutil.NomadClient(t)connects via the API;e2eutil.WaitForLeader(t, ...)andWaitForNodesReady(t, ...)gate execution until the cluster is ready. - Separation: e2e tests are in a separate top-level directory (
e2e/), not mixed with unit tests. No build tags are used to separate unit from integration — the distinction is by directory and test runner invocation.
WaitForResult pattern#
- Prevalence: Pervasive in async tests —
testutil.WaitForResult(testFn, errorFn)retriestestFnevery 10ms for500 * TestMultiplier()iterations (default 5 seconds in CI). - Assessment: Cleaner than
time.Sleep.TestMultiplier()readsNOMAD_TEST_SLOWNESSenv var to scale all timeouts uniformly. The newer code usesshoenig/test/waitwhich offers a fluent API for the same purpose.
Testify → shoenig/test migration#
- Both
testify/require(classic) andshoenig/test/must(newer) coexist. Newer tests and packages heavily prefermust.NoError(t, err),must.Eq(t, expected, actual)overrequire.NoError. The migration appears intentional —shoenig/testprovides more structured output and composableOptionerror messages viamust.Sprint(...).
Test quality observations#
What’s done well:
nomad/mock/as a DSL: The factory package produces realistic, fully-populated domain structs. Tests read naturally becausemock.Job()returns something that actually represents a valid job, not a zero-value stub. This pattern — a dedicated fixture factory — is one of the most valuable things about Nomad’s test infrastructure.- In-process full-server testing: Running real Raft, real state store, and real RPC handlers inside unit tests catches integration bugs that purely mocked tests would miss. The
TestServer(t, cb)pattern with a callback for config customization is ergonomic and flexible. - Conditional parallelism (
ci.Parallel): Thoughtful CI/local trade-off. 4,272 uses shows this is a genuine discipline, not an afterthought. - TestMultiplier / timing multiplier: Centralizing timing sensitivity in one env var (
NOMAD_TEST_SLOWNESS) is much better than ad-hoctime.Sleepcalls. All wait loops scale proportionally. - Fake drivers with configurable behavior:
drivers/mock/Drivercan be configured to block, crash, or delay via task config HCL — enabling realistic driver lifecycle testing without a real container runtime.
What could improve:
- Two assertion libraries: Simultaneous use of
testify/requireandshoenig/test/mustis a coherence cost. New contributors must know both. A full migration to one would reduce friction. - e2e test infrastructure dependency: The
e2e/tests implicitly require a running cluster but this is not enforced by a build tag — tests will fail with cryptic connection errors if run against no cluster. An explicit build tag (e.g.,//go:build e2e) would make the dependency explicit. - No mock generation: Hand-written fakes are thorough but expensive to maintain when interfaces evolve. Several mock types (e.g.,
ServiceRegistrationHandler) duplicate the interface signature manually.go generate+ mockery for the simplest interfaces could reduce boilerplate.
- Two assertion libraries: Simultaneous use of
Patterns worth emulating:
mock/as a domain fixture factory — separating “create a realistic object” from test logic; reused across hundreds of testsci.Parallel(t)pattern — environment-aware parallelism that respects CI resource constraintsTestServer(t, cb)with cleanup callback — in-process full-stack server witht.Cleanup(func(){...})teardown; no global state, each test gets its own serverWaitForResult/TestMultiplierfor async assertions — uniform timing scaling without scatteredtime.Sleepcalls