Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Testing and Benchmarking in Go
Go has a built-in testing framework provided by thetesting package and the go test command. This makes writing tests a natural part of the development workflow, not an external dependency.
Think of Go’s testing philosophy like a workshop where every tool comes in the same toolbox. There is no JUnit, no pytest, no Jest equivalent needed. The standard testing package and go test command handle test discovery, execution, parallelism, coverage, benchmarking, and fuzzing out of the box. Libraries like testify add assertion helpers, but the core machinery is built in. While other languages treat testing as an afterthought bolted on through third-party frameworks, Go treats it as a first-class citizen baked into the toolchain — the same way go fmt enforces formatting, go test enforces that testing is not optional.
Writing Tests
Test files reside in the same package as the code they test, but with a_test.go suffix. For example, math.go would be tested by math_test.go.
Test Functions
A test function must start withTest and take a single argument t *testing.T.
Table-Driven Tests
Go developers prefer “table-driven tests” to easily cover multiple test cases without repeating code. This is one of Go’s most distinctive testing idioms — you will see it in the standard library, in open-source projects, and in virtually every production codebase. The pattern is: define a slice of test cases, each with inputs and expected outputs, then loop through them.Subtests (t.Run)
The t.Run method allows you to define subtests. This is useful for hierarchical test reporting and running specific subtests via the -run flag.
Benchmarking
Go also has built-in support for benchmarking code to measure performance. Benchmark functions start withBenchmark and take b *testing.B. The runtime automatically adjusts b.N to run the function enough times for a statistically meaningful measurement — you do not choose the iteration count yourself.
Examples
Example functions serve two purposes: they act as documentation (appearing in Godoc) and as verified tests. They start withExample and use // Output: comments to verify results.
Fuzzing
Go 1.18 introduced native support for fuzzing, which generates random inputs to find edge cases and bugs. Think of fuzzing as hiring a chaos monkey for your code: instead of you carefully crafting test inputs, the fuzzer throws random, unexpected, and malformed data at your functions to see what breaks. It is particularly effective at finding panics, out-of-bounds errors, and edge cases that human testers would never think to write.Test Coverage
You can check code coverage with the-cover flag.
Test Helpers
Thet.Helper() method marks a function as a test helper. When a test fails inside a helper, the error message reports the caller’s file and line number instead of the helper’s, making failures much easier to locate:
Summary
go test: The standard command to run tests.TestXxx: Unit tests.- Table-Driven Tests: Idiomatic way to structure tests.
BenchmarkXxx: Performance tests.ExampleXxx: Documentation that is also tested.- Fuzzing: Automated random testing to find edge cases.
t.Helper(): Makes test helper functions report correct line numbers.-raceflag: Always use in development and CI to catch data races.
Interview Deep-Dive
Walk me through how you would structure tests for a service that talks to a database and an external API. What do you mock, what do you test with real dependencies, and why?
Walk me through how you would structure tests for a service that talks to a database and an external API. What do you mock, what do you test with real dependencies, and why?
Strong Answer:
- I use a layered testing strategy. Unit tests mock external dependencies using interfaces. Integration tests use real dependencies (a test database, a local API stub). End-to-end tests run the full stack.
- For the database: define a
UserRepositoryinterface with methods likeGetByID(ctx, id)andCreate(ctx, user). In unit tests, use a mock that implements this interface. In integration tests, use a real PostgreSQL instance (via Docker in CI, or testcontainers-go) with a clean database per test. The integration tests catch SQL syntax errors, schema mismatches, and transaction behavior that mocks cannot. - For the external API: define a
PaymentClientinterface. In unit tests, mock it. In integration tests, usehttptest.NewServerto create a local HTTP server that returns canned responses, testing your HTTP client’s parsing, error handling, and retry logic against realistic responses. - The key insight is that interfaces are your seams for testing. Every external dependency should be behind an interface defined by the consumer (not the provider). Your service takes the interface in its constructor, and tests inject mocks. This is constructor-based dependency injection without any framework.
- I avoid mocking the database for business logic tests. I have seen mocked tests pass while real queries fail due to migration bugs, NULL handling, or index behavior. The test database catches these.
t.Run. The pattern is prevalent because it makes adding new cases trivial (just add a row to the table), provides named subtests that show clearly in output (=== RUN TestAdd/negative_numbers), and allows running specific cases with -run. A good test table design includes: a descriptive name field (not “test1” — use the scenario being tested like “negative numbers” or “empty input”), all inputs and expected outputs in the struct, and optionally an expectErr bool or wantErr error field. Keep setup that is common to all cases outside the loop, and put case-specific setup inside the loop. If cases need different setup logic, it might be better as separate test functions rather than a table. The anti-pattern is a table with 50 fields per case where most are zero-valued — this means the cases are too heterogeneous for a single table.How do Go benchmarks work under the hood? What does `b.N` represent, and how do you avoid common benchmarking mistakes?
How do Go benchmarks work under the hood? What does `b.N` represent, and how do you avoid common benchmarking mistakes?
Strong Answer:
- When you run
go test -bench=., the testing framework calls your benchmark function multiple times with increasingb.Nvalues. It starts withb.N = 1, measures the time, then increasesb.N(2, 5, 10, 20, 50, 100, …) until the total benchmark time reaches a stable measurement (default 1 second, configurable with-benchtime). The framework reports the average time per operation: total time divided byb.N. - Common mistake 1: including setup in the timed section. If you allocate test data inside the
for i := 0; i < b.N; i++loop, you are benchmarking allocation plus the operation. Move setup before the loop and useb.ResetTimer()to exclude it. - Common mistake 2: the compiler optimizing away your benchmark. If the result of the benchmarked function is not used, the compiler may eliminate the call entirely. Assign the result to a package-level variable:
var result int; func BenchmarkFoo(b *testing.B) { var r int; for i := 0; i < b.N; i++ { r = Foo() }; result = r }. - Common mistake 3: not using
b.ReportAllocs()and-benchmem. Memory allocations are often the bottleneck in Go, and you cannot see them without these flags. A function that looks fast might be making thousands of heap allocations that pressure the garbage collector. - Common mistake 4: running benchmarks once and drawing conclusions. Use
benchstatwith-count=10runs to get statistically significant comparisons between before and after.
gopter) in that property-based testing generates inputs from a defined distribution and checks invariants you specify, while Go’s fuzzer is coverage-guided — it aims to maximize code coverage by mutating inputs intelligently. Fuzzing is particularly effective at finding edge cases in parsers, serialization code, and input validation. In practice, I write fuzz tests for any function that processes untrusted input (JSON parsing, URL handling, query parameter parsing) and run the fuzzer in CI for a fixed duration (30 seconds per fuzz target). The corpus of discovered interesting inputs is committed to the repo so regression testing is fast.Your test suite takes 8 minutes to run. Walk me through your approach to diagnosing and fixing slow tests in a Go codebase.
Your test suite takes 8 minutes to run. Walk me through your approach to diagnosing and fixing slow tests in a Go codebase.
Strong Answer:
- First, identify the slow tests. Run
go test -v -count=1 ./... 2>&1 | grep -E 'PASS|FAIL'to see per-package timing, then drill into slow packages withgo test -v -run . ./pkg/slow/to see individual test timing. The-count=1flag disables test caching so you get real timings. - Common culprits: tests that use
time.Sleepfor synchronization (replace with channels or condition variables), tests that start real servers and databases without reusing them across cases, tests that do not uset.Parallel()for independent tests, and integration tests mixed with unit tests. - Quick wins: add
t.Parallel()to every test that does not share state with other tests. This lets Go run them concurrently within a package and can cut test time dramatically. Usetesting.Short()and-shortflag to skip slow integration tests during local development. Use build tags to separate integration tests://go:build integrationso they only run in CI. - Medium-term: share expensive setup across tests using
TestMain(m *testing.M)for package-level setup (like starting a database container once), andt.Cleanup()for per-test teardown. Replace network calls withhttptest.NewServerstubs that respond instantly. - Long-term: if the test suite is fundamentally doing too much I/O, consider splitting into fast unit tests (seconds) and slow integration tests (minutes) with different CI stages. The fast suite runs on every commit, the slow suite runs on PR merge or hourly.
t.Helper() and t.Cleanup(). When do you use each?t.Helper() marks a function as a test helper. When a test fails inside a helper, the error message reports the caller’s file and line number, not the helper’s. This makes failure messages immediately useful instead of pointing to a generic assertEqual function. Call it as the first line in any test utility function. t.Cleanup(fn) registers a function to run after the test completes, even if the test calls t.Fatal(). It is better than defer because deferred functions do not run after t.Fatal() (which calls runtime.Goexit()), but cleanup functions always do. Use t.Cleanup for test teardown: deleting test data, stopping test servers, closing connections. Cleanup functions run in LIFO order, just like defers.