Chapter 8: Testing and tooling
This chapter introduction presents Go testing as more than a way to check finished code: tests can guide development, reveal performance problems, and find bugs in unexpected inputs.
After this chapter you will be able to
- Proactive role of tests. Explain how tests can both check that code still behaves as expected after changes and proactively guide improvements. Describe test-driven development as a workflow in which tests drive the code rather than being added afterward.
- Test file naming convention. Explain that Go tests belong in files named *_test.go, which normal builds ignore but testing compiles into the package.
- Test function discovery rules. Describe the name and argument conventions Go uses to run test functions, including func TestXxx(t *testing.T) with an uppercase-starting, unique Xxx. Explain that other test-related function types have their own conventions, and that examples are the exception to the single-argument rule.
- Go test execution. Describe how to run tests with go test and what it does with a package’s _test.go files. Explain that running it without arguments tests the current directory and that flags can change settings such as the timeout or which tests run.
- Default results and passing criteria. Explain what the PASS and ok lines report and why a test that does not explicitly fail can pass. Describe why ordinary go test output emphasizes failures rather than showing every passing test’s log.
- Go test function signature. State that a Go test function begins with Test, takes a *testing.T parameter, and returns no value; describe t as the source of tools for reporting a test’s result.
- Arrange act assert workflow. Describe the arrange, act, assert flow: set up test inputs, execute the code, and check the result against the expectation. Explain that a test failure should communicate both the expected and actual results.
- Test runner control through testing.T. Explain that go test supplies each test function with a *testing.T value, conventionally named t, and that tests report results through its methods rather than returning a result. Describe how this lets a test report multiple failures or stop when necessary.
- Fatal and non-fatal failure methods. Distinguish Fail, Error, and Errorf from FailNow, Fatal, and Fatalf: the first group marks a failure and continues, while the second marks a failure and exits the test immediately. Describe how Error and Fatal log messages, with their f variants accepting formatted messages.
- Failure output and stopping choice. Describe how go test reports failed tests with their logged messages and file-and-line annotations. Justify using non-fatal methods by default to reveal multiple problems, reserving fatal methods for a broken dependency or invariant that makes further testing impossible.
- Test-first development purpose. Explain that test-driven development starts with tests rather than implementation, giving the code measurable targets and letting you try the API early.
- Calculator behavior contract. Describe the Calculator API: Calculate takes an operation string and two integers, returns a result and an error, and supports add, sub, mul, and div; Store and Recall manage the last stored successful result. Explain that invalid operations and division by zero produce errors, and Store does nothing after a failed calculation.
- Black-box and open-box packages. Distinguish a test file declared as package basicmath_test, which treats basicmath as an outside dependency, from one declared as package basicmath, which can access package internals.
- Black-box testing preference. Justify using black-box tests wherever possible: they use the code as a user would, yielding more realistic results and fitting feature-focused TDD. Explain that open-box access can create impossible conditions, though accessing internals may sometimes be more efficient for regression tests.
- Sentinel error test cases. Explain why exported error types and sentinel errors deserve tests, and describe how to trigger the calculator’s two named errors: divide by zero and an unrecognized operation.
- Sentinel error verification. Describe performing an action expected to fail and using errors.Is to check that the returned error is the intended sentinel, failing the test if it is not.
- Actionable test failure messages. Describe how to write a brief failure message that explains why a test failed without repeating the test name or file location supplied by go test. State how to report expected and actual values, unexpected errors, and arguments used in repeated checks.
- Feature-level test targets. Explain why Store and Recall are tested together in TestMemory rather than only as separate methods, and describe the memory behavior the test verifies.
- Memory test setup and constraints. Trace the TestMemory checks from a new Calculator's initial memory value through a valid calculation to Store and Recall, and explain how asserting an initial value of zero adds a design constraint in TDD.
- Table-driven test structure. Describe how to define a test-case struct, put cases in a slice, and loop over the slice to run the same validation for each case. For Calculate, each case contains an operation string, two integer operands, and an expected integer result.
- Calculator state and invalid results. Explain why the Calculator holds two ints for the memory value and last calculation, plus an error that prevents Store from saving an invalid result.
- Calculator methods and test run. Describe the initial method implementation: Calculate uses a type switch and checks for a zero divisor, Store checks whether the last result was invalid, and Recall is straightforward. Explain that the implementation follows the tests and is then checked with go test.
- Coverage meaning and limits. Explain that code coverage is the percentage of a package’s statements executed by tests, and why it is worth checking during development without treating 100% as a universal target.
- Coverage profile inspection. Describe how to get a package’s coverage percentage with go test -cover, generate a profile with go test -coverprofile=<file name>, and inspect that profile in a browser with go tool cover.
- Missing Store error test. Trace how the coverage view exposed an untested Store error condition and how TestMemory was extended to check that an invalid calculation does not change stored memory but that storing can work afterward.
- Persistent calculator error bug. Explain how the new test revealed that Store stopped working after an error because Calculate did not reset its internal error value to nil after a successful calculation, and how fixing that made the tests pass.
- Direct handler response testing. Explain how the section tests a GET handler without running a server: pass it a sample request and an httptest recorder, then check the captured message and status code.
- Route testing with temporary servers. Explain that a mux attaches handlers to routes and directs requests by path, while unsupported paths get a not-found response; describe using httptest to start a temporary server on a random port and call it to verify routing.
- Checks beyond unit tests. Explain why a growing application may need checks beyond unit tests, and distinguish the questions benchmark testing and fuzz testing help answer.
- Benchmark purpose and fair comparison. Explain that benchmarks repeatedly run an operation under load to measure average performance, often comparing alternative implementations using the same parameters.
- Go benchmark execution and results. Describe using testing.B instead of testing.T for a benchmark and running it with go test -bench=.; explain that the results report iterations and nanoseconds per operation, alongside system information that affects performance.
- String generation comparison and limits. Describe the comparison between the baseline array-based random-string generator and a strings.Builder version, including that the baseline was faster in the reported results. Explain that benchmarks measure performance for the parameters tried, whereas fuzz tests are needed to search for bugs at unexplored edges.
- Purpose of fuzz testing. Explain that fuzz testing starts with valid example inputs, varies them to generate additional cases, and checks the results to uncover cases a developer might not think to test.
- Go fuzz test setup. Describe a Go fuzz test as a test function prefixed with Fuzz that takes testing.F and uses a corpus of examples as the basis for additional tests.
- Rune comparison bug and fix. Trace how fuzzing found an unexpected input that exposed a bug in Contains: converting the string to bytes was wrong for comparing runes, so the fix was to iterate over the string and compare runes.
What it builds on
- Package declarations (chapter 2) · Go package declarations are introduced.
- Initializing the project module (chapter 2) · Initializing a Go module with go mod init.
- Sentinel errors (chapter 7) · Sentinel errors represent identifiable error conditions.
- Iterating Unicode strings using a range loop (chapter 3) · Iterating over Unicode strings with a range loop
- The rune type (chapter 3) · What a rune represents
Section by section
Introduction
This chapter introduction presents Go testing as more than a way to check finished code: tests can guide development, reveal performance problems, and find bugs in unexpected inputs.
- Proactive role of tests: Explain how tests can both check that code still behaves as expected after changes and proactively guide improvements. Describe test-driven development as a workflow in which tests drive the code rather than being added afterward.
Why it matters: Tests can shape what you build, not just reassure you after you build it. - Benchmarking and performance insight: Describe how Go's benchmarking and profiling tools can help target performance improvements by providing CPU-usage and memory-allocation statistics, sometimes down to a line of code.
Why it matters: Measurements help you focus performance work where it matters. - Fuzzing input driven tests: Explain that Go's built-in fuzzer tests input-driven behavior more thoroughly by checking invariants against randomly generated data to search for breaking bugs.
Why it matters: Generated inputs can expose failures that hand-picked examples miss.
Key terms: testing, test-driven development, contracts, benchmarking, profiling, CPU usage, memory allocation, fuzzer, invariants, randomly generated corpus of data
8.1 Test files and functions
This section explains how Go recognizes test files and functions, and what to check when tests do not run.
- Test file naming convention: Explain that Go tests belong in files named *_test.go, which normal builds ignore but testing compiles into the package.
Why it matters: A correctly named file lets Go include your tests without including them in a normal build. - Test function discovery rules: Describe the name and argument conventions Go uses to run test functions, including func TestXxx(t *testing.T) with an uppercase-starting, unique Xxx. Explain that other test-related function types have their own conventions, and that examples are the exception to the single-argument rule.
Why it matters: Checking the function signature helps diagnose tests that do not run or produce a “no tests to run” warning.
Key terms: *_test.go, normal build process, go test, func TestXxx(t *testing.T), testing.T, no tests to run, go help testfunc
8.2 Running tests with go test
This section shows how go test runs a package’s tests, how to interpret its default results, and how verbose mode reveals individual test activity.
- Go test execution: Describe how to run tests with go test and what it does with a package’s _test.go files. Explain that running it without arguments tests the current directory and that flags can change settings such as the timeout or which tests run.
Why it matters: This is the starting point for running and controlling Go tests. - Default results and passing criteria: Explain what the PASS and ok lines report and why a test that does not explicitly fail can pass. Describe why ordinary go test output emphasizes failures rather than showing every passing test’s log.
Why it matters: Knowing what a quiet, successful run means helps you interpret test results without mistaking silence for a test that was skipped. - Verbose test output: Describe how go test -v confirms that an individual test ran and how to read its start line, result line, elapsed time, and intervening logs.
Why it matters: Verbose output lets you inspect test-by-test activity when the default summary is not enough.
Key terms: go test, _test.go, test executable, test flags, PASS, ok, panic, timeout, -v, verbose mode
8.3 Test functions
This section explains what Go test functions look like, how to structure and report a test, and how to choose between testing package internals and testing as a consumer.
- Go test function signature: State that a Go test function begins with Test, takes a *testing.T parameter, and returns no value; describe t as the source of tools for reporting a test’s result.
Why it matters: Go’s testing conventions determine which functions run as tests and how they report results. - Arrange act assert workflow: Describe the arrange, act, assert flow: set up test inputs, execute the code, and check the result against the expectation. Explain that a test failure should communicate both the expected and actual results.
Why it matters: A clear workflow and diagnostic failure message make tests useful for finding incorrect behavior. - Unit test isolation: Explain that an isolated test without external dependencies is a unit test because it tests a single unit of software, and that such tests often make up most of the tests developers write.
Why it matters: Isolation clarifies what the section means by a unit test. - Internal and consumer perspectives: Distinguish testing a package’s internal pieces from testing it as a library consumer, and state that Go permits the consumer perspective by putting test code in a separate package with the _test suffix.
Why it matters: The choice of test package affects which view of the code the tests exercise.
Key terms: Test, *testing.T, t parameter, arrange, act, assert, expectation, result, unit test, isolated, external dependencies, _test
8.4 Test life cycle
This section explains how Go tests report failures through testing.T, how fatal and non-fatal failure methods behave, and when to use each.
- Test runner control through testing.T: Explain that go test supplies each test function with a *testing.T value, conventionally named t, and that tests report results through its methods rather than returning a result. Describe how this lets a test report multiple failures or stop when necessary.
Why it matters: The t value is how a test communicates with the test runner. - Fatal and non-fatal failure methods: Distinguish Fail, Error, and Errorf from FailNow, Fatal, and Fatalf: the first group marks a failure and continues, while the second marks a failure and exits the test immediately. Describe how Error and Fatal log messages, with their f variants accepting formatted messages.
Why it matters: These methods determine whether a test can reveal further failures after the first one. - Failure output and stopping choice: Describe how go test reports failed tests with their logged messages and file-and-line annotations. Justify using non-fatal methods by default to reveal multiple problems, reserving fatal methods for a broken dependency or invariant that makes further testing impossible.
Why it matters: Continuing when possible gives you more useful debugging information from a single test run.
Key terms: testing.T, t, go test, Fail, Error, Errorf, FailNow, Fatal, Fatalf, fatal methods
8.5 Test-driven design: A calculator with a twist
This section introduces test-driven development through a calculator example, defining its behavior and setting up a compilable skeleton before writing tests.
- Test-first development purpose: Explain that test-driven development starts with tests rather than implementation, giving the code measurable targets and letting you try the API early.
Why it matters: Writing tests first can reveal whether an API is useful before its behavior is implemented. - Calculator behavior contract: Describe the Calculator API: Calculate takes an operation string and two integers, returns a result and an error, and supports add, sub, mul, and div; Store and Recall manage the last stored successful result. Explain that invalid operations and division by zero produce errors, and Store does nothing after a failed calculation.
Why it matters: The agreed behavior tells you what the calculator tests need to check. - Initial calculator skeleton: Describe creating a basicmath module and a calculator.go skeleton whose required methods exist but do not yet perform calculations or store results. Explain that this lets tests run before the functionality is implemented.
Why it matters: A compilable skeleton gives test-first work a starting point.
Key terms: test-driven development, TDD, API, Calculator, Calculate, Store, Recall, add, sub, basicmath
8.5.1 Black-box testing
This section sets up a Calculator test file and explains why tests written outside the target package are usually preferable to tests with access to package internals.
- Calculator test file convention: Describe how to name and place the Calculator test file: it goes in the basicmath directory, must end in _test.go, and is conventionally named calculator_test.go after the code file.
Why it matters: The filename lets Go recognize the test file and makes it easy for readers to find. - Black-box and open-box packages: Distinguish a test file declared as package basicmath_test, which treats basicmath as an outside dependency, from one declared as package basicmath, which can access package internals.
Why it matters: The package declaration determines whether a test exercises the public-facing behavior or can reach unexported code. - Black-box testing preference: Justify using black-box tests wherever possible: they use the code as a user would, yielding more realistic results and fitting feature-focused TDD. Explain that open-box access can create impossible conditions, though accessing internals may sometimes be more efficient for regression tests.
Why it matters: Choosing the test boundary helps avoid reassuring results from scenarios users could never encounter.
Key terms: *_test.go, calculator_test.go, basicmath, black box testing, open box testing, white box testing, clear box testing, package <package name>_test, unexported code, TDD
8.5.2 Writing tests for error conditions
This section shows how to test a calculator’s named error conditions before its implementation is complete.
- Sentinel error test cases: Explain why exported error types and sentinel errors deserve tests, and describe how to trigger the calculator’s two named errors: divide by zero and an unrecognized operation.
Why it matters: Reproducing each error condition makes failures predictable and testable. - Sentinel error verification: Describe performing an action expected to fail and using errors.Is to check that the returned error is the intended sentinel, failing the test if it is not.
Why it matters: A test should verify which error occurred, not merely that something went wrong. - Invalid inputs and initial failures: Describe looping over several invalid operation strings and logging each case for context; explain why go test is expected to report many failures while Calculate is still unimplemented.
Why it matters: Contextual logs help identify failing inputs, while initial failures provide targets for implementation.
Key terms: exported error types, sentinel errors, ErrDivByZero, ErrUnknownOperation, errors.Is, t.Log, t.Logf, go test, test-driven workflow
8.5.3 Writing good test logs
This section shows how to write brief, diagnostic Go test failure messages and avoid failures that provide no useful log output.
- Actionable test failure messages: Describe how to write a brief failure message that explains why a test failed without repeating the test name or file location supplied by go test. State how to report expected and actual values, unexpected errors, and arguments used in repeated checks.
Why it matters: A useful failure message may be the only clue you have when debugging a failed test. - Silent test failure methods: Explain why t.Fail and t.FailNow should have log messages immediately nearby when used: neither method logs an explanatory message itself.
Why it matters: A test that fails without an accompanying message gives you little help locating the cause.
Key terms: go test, want, got, t.Log, t.Logf, t.Fail, t.FailNow
8.5.4 Choosing testing targets
This section shows how to choose a feature-level test target for calculator memory and run that test by itself.
- Feature-level test targets: Explain why Store and Recall are tested together in TestMemory rather than only as separate methods, and describe the memory behavior the test verifies.
Why it matters: A test can target a complete behavior when its methods naturally depend on one another. - Memory test setup and constraints: Trace the TestMemory checks from a new Calculator's initial memory value through a valid calculation to Store and Recall, and explain how asserting an initial value of zero adds a design constraint in TDD.
Why it matters: Test assertions can define behavior the implementation must provide, not merely check behavior already specified. - Focused test execution: Describe how to run only TestMemory with go test -run and state how the flag selects tests.
Why it matters: Running one relevant test avoids noise from other failing tests during development.
Key terms: Store, Recall, TestMemory, calculator memory feature, initial value, 0, Calculate, t.Logf, fatal, TDD workflow
8.5.5 Table-driven testing
The section shows how to test many Calculate inputs without repeating the same validation code by using table-driven tests.
- Table-driven test structure: Describe how to define a test-case struct, put cases in a slice, and loop over the slice to run the same validation for each case. For Calculate, each case contains an operation string, two integer operands, and an expected integer result.
Why it matters: This structure lets you test many operations and arguments while writing the validation logic only once. - Benefits of shared test logic: Explain why moving repeated test logic into a loop makes the tests easier to read and maintain, and how a case’s stored values can supply a log header.
Why it matters: Grouping cases makes it easier to see what is tested and to add or remove cases without duplicating code.
Key terms: table-driven testing, testCase struct, slice, Calculate method, test cases, testCase
8.5.6 Implementation and initial testing
The section implements a calculator from its tests, then runs go test to check the initial implementation.
- Calculator state and invalid results: Explain why the Calculator holds two ints for the memory value and last calculation, plus an error that prevents Store from saving an invalid result.
Why it matters: The stored error connects a failed calculation to what Store is allowed to save. - Calculator methods and test run: Describe the initial method implementation: Calculate uses a type switch and checks for a zero divisor, Store checks whether the last result was invalid, and Recall is straightforward. Explain that the implementation follows the tests and is then checked with go test.
Why it matters: This shows how the previously written tests guide implementation and provide an immediate check.
Key terms: Calculator, memory value, last calculation, Store, Calculate, Recall, type switch, go test, test-driven development
8.5.7 Code coverage
This section shows how to measure and inspect Go test coverage, then use uncovered code to find missing tests and a calculator bug.
- Coverage meaning and limits: Explain that code coverage is the percentage of a package’s statements executed by tests, and why it is worth checking during development without treating 100% as a universal target.
Why it matters: Coverage can point you toward missed cases, even when complete coverage is impractical. - Coverage profile inspection: Describe how to get a package’s coverage percentage with go test -cover, generate a profile with go test -coverprofile=<file name>, and inspect that profile in a browser with go tool cover.
Why it matters: A visual coverage report shows which statements to investigate, not just the overall percentage. - Missing Store error test: Trace how the coverage view exposed an untested Store error condition and how TestMemory was extended to check that an invalid calculation does not change stored memory but that storing can work afterward.
Why it matters: An uncovered branch can suggest a specific behavior to test rather than merely a higher percentage to pursue. - Persistent calculator error bug: Explain how the new test revealed that Store stopped working after an error because Calculate did not reset its internal error value to nil after a successful calculation, and how fixing that made the tests pass.
Why it matters: The example shows why running meaningful assertions matters even when coverage reaches 100%.
Key terms: code coverage, statements, go test -cover, go test -coverprofile, coverage profile, go tool cover, Store, Calculator, TestMemory, Calculate
8.6 Simulating dependencies with an HTTP server
This section shows how to test HTTP handlers directly with httptest recorders and through a temporary server that routes requests.
- HTTP requests and handlers: Describe the request information a Go handler receives—such as the URL and route, method, optional body, and headers—and how a handler writes a response and status code.
Why it matters: These pieces explain what the HTTP tests send and what they check. - Direct handler response testing: Explain how the section tests a GET handler without running a server: pass it a sample request and an httptest recorder, then check the captured message and status code.
Why it matters: A recorder lets you verify a handler’s output without setting up a running server. - POST body and table tests: Describe how the POST handler reads the request body, returns an error code if reading fails, and otherwise appends the supplied data to “hello”; explain why a table test suits checking multiple handlers and responses.
Why it matters: The POST example extends the same testing pattern to incoming data and multiple cases. - Route testing with temporary servers: Explain that a mux attaches handlers to routes and directs requests by path, while unsupported paths get a not-found response; describe using httptest to start a temporary server on a random port and call it to verify routing.
Why it matters: Testing through a server checks route selection as well as the handlers’ responses.
Key terms: URL, route, GET, POST, body, header, httptest, recorder, GET handler, hello
8.7 Beyond unit tests
The section introduces benchmark and fuzz testing as ways to check performance and uncover missed cases beyond ordinary unit tests.
- Checks beyond unit tests: Explain why a growing application may need checks beyond unit tests, and distinguish the questions benchmark testing and fuzz testing help answer.
Why it matters: These two kinds of testing address questions that checking expected function results alone may not answer.
Key terms: unit tests, benchmark testing, fuzz testing, Go’s built-in testing library
8.7.1 Benchmark testing
This section shows how to use Go benchmarks to compare two string-generation methods, read the results, and recognize what benchmarking cannot reveal.
- Benchmark purpose and fair comparison: Explain that benchmarks repeatedly run an operation under load to measure average performance, often comparing alternative implementations using the same parameters.
Why it matters: Keeping the inputs consistent makes a performance comparison meaningful. - Go benchmark execution and results: Describe using testing.B instead of testing.T for a benchmark and running it with go test -bench=.; explain that the results report iterations and nanoseconds per operation, alongside system information that affects performance.
Why it matters: These are the tools and measurements needed to reproduce and interpret a Go benchmark. - String generation comparison and limits: Describe the comparison between the baseline array-based random-string generator and a strings.Builder version, including that the baseline was faster in the reported results. Explain that benchmarks measure performance for the parameters tried, whereas fuzz tests are needed to search for bugs at unexplored edges.
Why it matters: Measured results can guide optimization, but they do not establish that code is free of bugs.
Key terms: benchmark testing, average processing time, parameters, testing.B, testing.T, go test -bench=., iterations, nanoseconds per operation, baseline, strings.Builder
8.7.2 Fuzz testing
This section introduces Go fuzz testing by using generated cases to find and fix a string-handling bug that ordinary tests might miss.
- Purpose of fuzz testing: Explain that fuzz testing starts with valid example inputs, varies them to generate additional cases, and checks the results to uncover cases a developer might not think to test.
Why it matters: Fuzzing helps reveal overlooked edge cases without requiring you to invent every input yourself. - Go fuzz test setup: Describe a Go fuzz test as a test function prefixed with Fuzz that takes testing.F and uses a corpus of examples as the basis for additional tests.
Why it matters: These are the conventions that let Go's testing tools run a fuzz test. - Rune comparison bug and fix: Trace how fuzzing found an unexpected input that exposed a bug in Contains: converting the string to bytes was wrong for comparing runes, so the fix was to iterate over the string and compare runes.
Why it matters: The example shows how a generated case can point to a specific implementation mistake. - Fuzz run duration: State that fuzzing can continue indefinitely unless its run time is limited with the -fuzztime flag.
Why it matters: A time limit keeps a fuzz run from continuing longer than intended.
Key terms: fuzz testing, corpus, FuzzContains, testing.F, Contains, rune, byte array, -fuzztime
8.8 Summary
The chapter summary recaps Go’s test files and runner, test-driven development, ways to improve test suites, and benchmark and fuzz testing.
Where this is used later
- API module: REST service (chapter 11) · The REST service uses requests, routes, handlers, and responses.
- Testing the complete system (chapter 11) · Testing the complete system builds on checking HTTP behavior against expected responses.
Concepts explained in this chapter
- go testgo test -cover is the Go test command mode that runs package tests and reports the percentage of statements they execute.
- test-driven developmentA test-driven workflow is a development process that begins with failing automated tests and incrementally implements code until the tests pass.
- table-driven testA table-driven test represents multiple test cases as data records and runs a common test procedure over them.
- benchmarkBenchmark testing measures how code performs under repeated execution with controlled inputs and conditions.
- fuzzingFuzzing automatically tests software with generated or mutated inputs to find failures and unexpected behavior.
- test functionA test function is a Go function recognized by the testing tool as an executable test according to its required naming and signature convention.
- code coverageCode coverage is a measurement of which portions or execution paths of a program are exercised by tests.
- HTTP multiplexerAn HTTP multiplexer is a routing component that selects handlers for requests according to their paths.
- endpointAn HTTP endpoint is a network-accessible interface identified by an HTTP method and resource location that handles client requests.
- httptesthttptest is a Go standard-library package that provides utilities for testing HTTP servers and handlers.
- httptest.ResponseRecorderhttptest.ResponseRecorder is a Go test utility that records an HTTP handler's response for inspection.
- test serverA test server is a temporary server instance created to exercise server behavior under controlled test conditions.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
