stevenacoffman/testing-legacy
Go testing practice and legacy code: table tests, helpers, fuzzing, and safe change.
Use when designing a test strategy for parsers, serializers, protocol handlers, file uploads, crypto or TLS code, or any component that processes untrusted external input, to decide whether fuzzing is needed, which components to prioritize, and how to run fuzzing in CI.
Use when multiple backends or plugins must pass the same multi-step acceptance scenario, or when tempted to build a separate test binary or external runner. Build a reusable TestCase harness as a Go library so go test drives every backend with -run, -race, and -cover.
Use when exploring a new library or API, or building a feature whose design is not yet known, and asking whether to write tests first, how to do TDD without a stable interface, or when to refactor a growing, untested main or shell into a tested functional core.
Use when writing a Go test for a function with stateful external dependencies and multiple input scenarios. Combine a named map-based test table with a handwritten fake collaborator so each failure names its scenario and asserts on observable state, not call sequences.
Use when a Go test starts a server in a goroutine via run() and may send requests before it is listening. Instead of sleeps or channels, add a /healthz or /readyz endpoint and a waitForReady helper that polls until 200 or context cancellation; it doubles as a production probe.
Use when Go tests mock net.Conn, net.Listener, or net.Dial, or when choosing between loopback TCP, net.Pipe, bufconn, and httptest for testing connection-oriented protocols. Covers the testConn helper and running a real SSH server in-process.
Use when writing or reviewing Go test helpers that return errors, or when tests repeat setup error checks. Helpers should call t.Fatalf instead of returning errors, call t.Helper() for correct line numbers, and register teardown with t.Cleanup() or a returned cleanup func.
Use when Go code calls exec.Command and tests need to fake the external binary or its failures (non-zero exit, garbage stdout, crash). Inject a command factory and re-run the test binary via TestHelperProcess and GO_WANT_HELPER_PROCESS=1. Not for HTTP services or databases.
Use when deciding what kind of test to write for a piece of logic (domain function, repository adapter, HTTP or gRPC endpoint wiring, cross-service contract), or when designing a test strategy for a new service.
Use when other packages need to test against a Go package's interface, or duplicate setup because _test.go files cannot be imported. Export test factories (TestServer, TestConfig), compliance suites, and mocks from a regular testing.go file.
Use when a Go test waits on a goroutine, channel, or event and times out in CI but passes locally, or someone proposes a Clock interface. Use select with time.After scaled by a package-level timeMultiplier set larger in CI. Not when asserting the order of timed events.
Use when deciding where to place tests to detect the effects of planned changes in legacy code, especially when changes span several classes and you want one test location (pinch point) that covers them all, or are unsure a passing test actually covers the change point.
Use when changing code that has no or insufficient tests: adding a feature, fixing a bug in code you fear touching, refactoring legacy code, or getting a class with heavy constructor dependencies under test. Not for greenfield TDD or code already well covered.
Use when property-based tests are hard to write, properties keep needing exception clauses, invariants are hard to state universally, or when choosing between example-based and property-based testing. Treat that difficulty as design feedback. Not for library or API usage questions.
Use when reading a large, tangled legacy module has not produced a working mental model: refactor aggressively on a throwaway branch (extract, rename, reorganize) purely to understand the control flow and find seams, then discard the branch before making the real change.
Use when adding new behavior to existing code that cannot be tested right away and choosing among Sprout Method, Sprout Class, Wrap Method, or Wrap Class.
Use when deciding how much testing to invest in a system based on its deployment model: tended (hotfixable, like cloud services) vs untended (firmware, embedded, regulated) and transient vs long-lived code, or when comparing test practices across those system types.
Use when tests are not helping: high coverage but bugs still ship, a slow suite nobody trusts, TDD not catching regressions, refactoring breaking tests, or choosing between unit, integration, and acceptance tests. Covers the purpose and mix of a test suite.