Testing¶
Coverage sits above 90%, the race detector is part of CI, and behaviour that spans components is covered by godog scenarios. None of that is the interesting part.
The interesting part is that this module has repeatedly had tests that passed for the wrong reason: green because of an unrelated bug, or because the harness was broken rather than the code. Most of this page is about recognising that.
Running things¶
just # tidy, lint, test: before every push
just ci # the full local CI: tidy, test, test-race, lint
just test-race # the race detector on its own
just coverage # HTML coverage report
The race detector is not optional. Notification ordering, the settle window, the
ObservedSection binding and the watcher degradation path are all concurrent, and a race
in any of them is a real defect a user will eventually hit.
Shape of a test¶
- Parallel by default.
t.Parallel()unless there is a reason not to. There is no global state to fight over, deliberately. - In-memory filesystems.
memFS(t, files)(afero'sMemMapFsbehind aconfig.FSadapter) for anything that would otherwise touch disk. afero is used only by tests, deliberately: it is a good in-memory filesystem and it stays out of what consumers inherit. Reserve real files for the handful of cases exercising a genuine OS watcher, andconfig.Dir(t.TempDir())where a real rooted directory is wanted. - Table-driven for variations, individual functions for distinct guarantees. When a test asserts several independent promises, split it, so a failure names which promise broke and not just that something did.
- Name the guarantee, not the mechanism.
TestReadsNeverStraddleAReloadsays what breaks if it fails;TestView2does not.
Test helpers live alongside the tests that use them; storeOn and memFS are
the common ones. The afero adapter lives in fs_afero_test.go, which also exports
NewMemFS for the external config_test package.
Failure modes to watch for¶
Each of these is drawn from a real defect in this repository, not a hypothetical.
A test passing because of another bug¶
A stale write fingerprint made every second write fail. That masked an unbounded observer cascade, because the cascade could never get past its second iteration. Both were real bugs, and each hid the other. Fixing the fingerprint made the cascade visible.
What to do: when a test passes, know which line makes it pass. If you cannot say, break that line deliberately and check the test fails.
A test that is vacuously true¶
TestApply_ValidationAgreesWithTheReloadItCauses used a single file and no environment
layer, so the two things it claimed to compare were trivially identical. It could not have
failed.
Another asserted a write created a missing file, but used a flat scalar, where the nesting bug it was supposed to catch was impossible to reach.
What to do: ask what change to the production code would make this test fail. If the answer is "none", it is not a test.
A broken harness reading as a clean pass¶
Three times this has produced a confident, wrong conclusion:
- Measuring read coherence with
os.WriteFile, which truncates before writing. Both libraries under test were being measured against a writer that exposed half-written files. The module appeared to tear 12,407 times. It does not. Writing atomically (temp file, then rename) gave 0. - A negative control that injected an OpenTelemetry package to prove a dependency guard
would catch it. It produced no output, which read as "not caught". In fact it was a build failure
from a bad escape in the harness, and
grepsimply had nothing to match. - A multi-file watch measurement that showed one notification and looked correct. Both writes had landed before the first reload ran. Varying the gap showed two.
What to do: when measuring anything involving concurrent file writes, make the writes atomic first. When a check produces no output, confirm it ran. And watch every guard fail at least once, because a guard nobody has watched fail is a guard nobody knows works.
Doc comment and test encoding the same confusion¶
A fidelity test asserted a quoted YAML value round-tripped in its source form, and the doc comment above the function said the same thing. Both were wrong in the same way, so the test confirmed the comment and neither confirmed the behaviour.
What to do: a test derived from the same misunderstanding as the code is not independent evidence. Where a guarantee matters, assert the observable outcome a user would see, not the intermediate representation.
A test that forbids a correct outcome¶
TestAddLayer_AFailingLayerWithdrawsOnlyItself asserted that a concurrent good layer
always succeeds. It does not, and should not: both calls adopt a backend then reload every
backend, so if the bad layer is registered when the good one reloads, that reload fails.
That is correct, because reloading is fail-closed. The test failed roughly once in several
hundred full-suite runs, and the failure was correct.
What to do: under concurrency, assert the invariant rather than one interleaving's outcome. Here the invariant is "a caller told its layer was added must find it contributing", which holds whichever order things happen in. Where practical, add a deterministic sibling test that fails every time rather than occasionally.
Conformance suite for codec adapters¶
A format adapter (config-json, config-toml, or one you write) proves it behaves like a
first-class backend by running the exported conformance suite against its codec, in one test:
func TestConformance(t *testing.T) {
conformance.Run(t, conformance.Suite{
Codec: jsonfile.Codec{},
Sample: []byte(`{"server":{"port":"9090"}}`),
Defines: map[string]string{"server.port": "9090"},
WriteKey: "server.host", WriteValue: "localhost",
})
}
Run executes one named subtest per contract, so a failing adapter sees exactly which one
it breaks. Every codec gets the capability split, decode-and-merge, absent-source tolerance
and provenance. A read-only codec also has its layer checked as skipped by write routing.
An EditingCodec gets the write round-trip, the no-edit no-op and the ErrConflict
the seam exists to make unmissable instead. It uses
stdlib testing only, so running it costs an adapter no assertion-library dependency.
The suite lives in conformance/ and is itself tested in conformance/conformance_test.go
against trivial in-package codecs, which is also where its teeth are confirmed: break a codec's
Apply and the relevant subtest fails. The core's own seam coverage stays independent of it in
codec_test.go, so a suite that ever passed wrongly would not take the core's checks down with
it. What the suite does not assert, such as multi-document provenance and exact scalar
types, is format-specific and lives in each adapter's own fidelity tests.
Testing the documentation¶
Documentation claims are tested, because they are the claims a user actually relies on.
| Test | Pins |
|---|---|
docsclaim_test.go |
the worked examples and claims in the docs, such as the before/after on the landing page |
coherence_test.go |
the read-coherence guarantee: no read straddles a reload |
observer_contract_test.go |
the observer guarantees, one per test |
custombackend_test.go |
every snippet in the custom-backend guide, including the write path |
fidelity_test.go |
the comment- and structure-preservation contract |
depfootprint_test.go |
the dependency-footprint claim |
If you change behaviour one of these describes, the test failing is the reminder to update the prose. That is the point.
A mechanical check worth re-running after editing guides is to extract every Go block and confirm it parses either as statements or as declarations. Blocks that parse neither way mix top-level declarations with statements, and cannot be made valid wherever a reader puts them. That check has caught four such blocks, one of them added minutes earlier in the same editing pass.
BDD scenarios¶
features/store/ holds godog scenarios for behaviour that spans components and time:
reload, concurrency, watching, the typed-section contract. A scenario earns its place when
the behaviour is hard to see in a unit test: several components, an ordering, a failure
that has to propagate somewhere specific. bdd_test.go runs them inside the ordinary
go test job, and BDD_TAGS selects a single feature or scenario.
Steps live in steps_*_test.go, grouped by area. Keep step definitions thin: they should
drive the public API and assert, not contain logic of their own.
Coverage¶
Above 90%, and it is a floor rather than a target. Coverage measures which lines ran, not whether anything was verified, and every failure mode on this page occurred in code that was covered.
The useful question is not "is this line covered" but "would a test fail if this line were
wrong". just coverage shows the first; only reading the assertions shows the second.