Switchyard's tests live beside the code in switchyard/ as *_test.go files. They exist to guarantee three things for every stage/feature:
- Flow / IO — the real request→response path works and produces the accepted output.
- Default behavior — the built-in implementation is correct (e.g. round-robin actually rotates).
- Custom behavior — a user-supplied implementation assigned to the stage's field is honored and behaves as intended.
The Makefile wraps these (make test, make race, make cover, make ci). Raw commands:
go test ./... # run everything (make test)
go test ./switchyard/ # just the library
go test -run TestDecide ./... # a subset by name
go test -race ./... # detect data races (round-robin is lock-free) (make race)
go test -cover ./... # coverage summary (make cover)go vet ./... and gofmt -l . should always be clean (make lint). If you add or change a command here, update the Makefile too.
Two Go packages coexist in the switchyard/ directory:
package switchyard(white-box) — for unexported pure functions only: request capture, variable/template resolution, log-format compilation and rendering. Files:request_test.go,vars_test.go,logging_internal_test.go.package switchyard_test(black-box) — for everything behavioral. It drives only the exported API (New,Handler, the interface fields), so it exercises exactly what an SDK user sees. This is most of the suite: one file per stage (decide_test.go,selector_test.go,router_test.go,pool_test.go,headers_test.go,static_test.go,actor_test.go,logging_test.go), plusproxy_test.go(validation + end-to-end) andexample_test.go(compile-checked doc snippets).
Prefer black-box tests: they prove the public contract and can't accidentally depend on internals. Reach for white-box only for pure helpers with no public entry point.
- Fake backends —
newEchoBackend(t, id)starts a realhttptest.Serverthat records the headers it received and echoes itsid. Use it to assert routing, load distribution, header injection, andX-Forwarded-For. - Driving a request —
serve(p, method, target)runs one request throughp.Handler()against anhttptest.NewRecorder()(the proxy runs in-process; only backends are real servers). Returns the recorder for status/body assertions. - Custom-behavior fakes — small types implementing a stage interface (
recordingLogger,fixedSelector,teapotDecider,headerRouter,filterPool,sentinelStatic,sentinelActor, …). Assign one to the correspondingProxy/Locationfield, then assert it's honored.
- Pure stage (Decide, Router, Selector, Pool): call the method directly with a constructed
Requestand assert the returnedDecision/*Location/*Backend. No I/O needed. - Side-effecting stage (Actor, StaticServer): drive through
Handler/serve(or callAct/Servewith a recorder) and assert status + body. - Config-driven default: build a
Config, callNew, and assert the default's observable behavior (e.g. round-robin alternation, headers reaching the backend). - Custom override (axis 3): after
New, assign your fake top.<Stage>orloc.<Stage>and assert the new behavior — this is the same thing an SDK user does. - Logger output: there's no writer injection, so either unit-test
logFormat.render(white-box) or configure afileoutput to at.TempDir()path and read it back (black-box).
Any change to a stage's logic MUST come with tests. Specifically:
- Changing an existing stage's behavior → update/extend its
*_test.goso the three axes (flow, default, custom) still hold, and add a case for the new behavior. - Adding a new pluggable stage → ship a
<stage>_test.gowith: a default-behavior test, a custom-override test (assign a fake to the field and assert it's honored), and at least one assertion of the stage in the end-to-end flow (proxy_test.go). - Fixing a bug → add a test that fails before the fix and passes after.
A quick way to confirm a test actually asserts behavior (not just executes it): temporarily break the code it covers and check the test fails.
Keep tests table-driven where it reduces repetition, name them Test<Stage><Behavior>, and use t.TempDir() for any filesystem needs so nothing leaks between runs.