Chapter 14: Testing and building your code from tests
The introduction explains why growing Rust programs need structure and tests, then outlines the test-driven development cycle.
After this chapter you will be able to
- Tests beyond compiler checks. Explain why tests remain useful even with Rust’s strict compiler, including what they can reveal after a code change.
- Tests-first development cycle. Describe the order of work in test-driven development as presented here: write failing tests first, then write code that makes them pass one by one.
- Crates and module spaces. Explain that Rust code is written in a crate made up of one or more files, and that a module is a space within the code for related items such as functions and structs.
- Module organization and names. Explain how modules help organize growing code, keep similar names from conflicting, and make an item's location informative to readers.
- Module privacy and encapsulation. Explain that items inside a module and modules themselves start private, and that making selected things public lets you limit what users can access directly.
- Module declaration and scope. Describe how to create a module with mod and a code block, and explain why the example imports Display inside print_things rather than at the top of the file.
- Private and public functions. Explain why main cannot call prints_one_thing when it is declared with fn, and what change makes the call work.
- Public structs and private fields. Explain that `pub` on a struct makes the type public but does not make its fields public. Describe how a field or tuple-struct field needs its own `pub`, using `Email(String)` or `Billy` as an example.
- Public enum and trait contents. Distinguish `pub` on an enum or trait from `pub` on a struct: a public enum’s variants and a public trait’s methods are public by default.
- Module visibility across boundaries. Explain that a top-level module is available within its own crate by default but needs `pub` to be accessible from outside, and that nested modules each need `pub` to be public.
- Child module access. Explain why code in a child module can use items in its parent modules without those items being marked pub.
- Absolute and parent paths. Distinguish a path beginning with crate:: from one using super::, and describe how both can reach the same function in the nested example.
- Project files and module declarations. Describe the usual roles of main.rs and lib.rs, and explain what must happen before Rust includes code from a separate file such as functions.rs.
- Test placement flexibility. Explain that Rust lets you write tests next to your code, while separate test files are optional.
- Marking and running tests. Explain how to turn a function into a test and run tests in the Playground or with Cargo.
- Passing and failing tests. Explain how Rust decides whether a test passes or fails and how assert_eq! can cause failure.
- Failed assertion messages. Explain what happens when an `assert_eq!` test compares unequal values and how its failure output helps identify the mismatch.
- Backtraces for difficult failures. Describe when to enable `RUST_BACKTRACE`, how to enable it, and how the section suggests finding your code in its output. Distinguish the ordinary backtrace from the more verbose `full` setting.
- Multiple test functions. Explain that you can write several test functions to check ordinary functions, and that each test function still needs its own #[test] attribute.
- Test-only module setup. Describe how to put tests in their own module using mod and #[cfg(test)], while keeping #[test] on each test and importing the surrounding functions with use super::*;.
- Test-driven development cycle. Explain the test-driven development cycle: write tests that initially fail, write code until they pass, and keep the tests to catch problems when the code changes later.
- Subtractor test cases. Describe the behavior the tests require of the Subtractor, including ordinary subtraction, consecutive minus signs, trailing spaces and minus signs, and invalid input.
- Placeholder method and test feedback. Explain why the first version has an empty Subtractor struct and a math() method returning a placeholder i32, and describe what the initial test run reports.
- Accumulating numbers from input. Describe how the Subtractor collects digits into num_to_parse, parses a completed number as an i32, and updates a running total.
- Alternating subtraction operations. Describe how the Operation enum represents addition or subtraction and switches variants for each minus sign.
- Processing the final number. Explain why the final collected number was initially ignored and how a final check and shared .do_operation() method fix it.
- Test-driven development cycle. Trace the back-and-forth process of writing tests, implementing code to make them pass, and adding tests as new cases come to mind.
What it builds on
- Structs (chapter 4) · the basic structure of a struct
- Enums (chapter 4) · enums and their variants
- Traits: The basics (chapter 7) · traits and their methods
Section by section
Introduction
The introduction explains why growing Rust programs need structure and tests, then outlines the test-driven development cycle.
- Separation of growing code: Explain why growing programs benefit from putting some code in a space separate from other code.
Why it matters: Recognizing when code needs separation helps you think about a growing program’s structure. - Tests beyond compiler checks: Explain why tests remain useful even with Rust’s strict compiler, including what they can reveal after a code change.
Why it matters: Passing compilation does not mean your program behaves as intended. - Tests-first development cycle: Describe the order of work in test-driven development as presented here: write failing tests first, then write code that makes them pass one by one.
Why it matters: The order of writing tests and code is what distinguishes the TDD approach described here.
Key terms: structure, testing, logic errors, compiler, test-driven development, TDD
14.1 Crates and modules
Introduces crates as the files that make up Rust code and modules as a way to organize names and control access as code grows.
- Crates and module spaces: Explain that Rust code is written in a crate made up of one or more files, and that a module is a space within the code for related items such as functions and structs.
Why it matters: This distinction gives you a starting point for deciding where related Rust code belongs. - Module organization and names: Explain how modules help organize growing code, keep similar names from conflicting, and make an item's location informative to readers.
Why it matters: A module path can tell you both which item is meant and where to look for related code. - Module privacy and encapsulation: Explain that items inside a module and modules themselves start private, and that making selected things public lets you limit what users can access directly.
Why it matters: Privacy lets you decide which parts of your code others can use directly.
Key terms: crate, module, mod, namespace, std::collections::HashMap, private, public, pub, encapsulation
14.1.1 Module basics
Shows how to declare a Rust module, why an import must be inside it, and how to make one of its functions callable from main.
- Module declaration and scope: Describe how to create a module with mod and a code block, and explain why the example imports Display inside print_things rather than at the top of the file.
Why it matters: Module boundaries affect where names are available to your code. - Private and public functions: Explain why main cannot call prints_one_thing when it is declared with fn, and what change makes the call work.
Why it matters: Making a function public is what lets the example call it from outside its module.
Key terms: mod, module, use std::fmt::Display;, print_things, pub, fn, pub fn, private, prints_one_thing, main()
14.1.2 More on how the pub keyword works
This section explains how `pub` affects the visibility of structs, enums, traits, and modules differently.
- Public structs and private fields: Explain that `pub` on a struct makes the type public but does not make its fields public. Describe how a field or tuple-struct field needs its own `pub`, using `Email(String)` or `Billy` as an example.
Why it matters: It lets you expose a type without exposing every part of its data. - Public enum and trait contents: Distinguish `pub` on an enum or trait from `pub` on a struct: a public enum’s variants and a public trait’s methods are public by default.
Why it matters: The visibility rule depends on the kind of item marked `pub`. - Module visibility across boundaries: Explain that a top-level module is available within its own crate by default but needs `pub` to be accessible from outside, and that nested modules each need `pub` to be public.
Why it matters: It helps you understand why code in the same crate can reach a module that outside code cannot.
Key terms: pub, struct, tuple structs, parameters, enum, variant, trait, method, module, crate
14.1.3 Modules inside modules
This section explains how nested modules access their parents, how module paths work, and how declaring a separate file makes Rust include it in a project.
- Child module access: Explain why code in a child module can use items in its parent modules without those items being marked pub.
Why it matters: It explains why nested code can use a parent’s functions even when they are not public. - Absolute and parent paths: Distinguish a path beginning with crate:: from one using super::, and describe how both can reach the same function in the nested example.
Why it matters: These two path styles let you describe where an item is from different starting points. - Project files and module declarations: Describe the usual roles of main.rs and lib.rs, and explain what must happen before Rust includes code from a separate file such as functions.rs.
Why it matters: A file’s code cannot take part in the project until Rust is told to include its module.
Key terms: child module, parent module, pub, crate::, super::, module, main.rs, lib.rs, functions.rs, mod functions;
14.2 Testing
This section introduces Rust testing by explaining that tests can live alongside the code they test rather than in separate files.
- Test placement flexibility: Explain that Rust lets you write tests next to your code, while separate test files are optional.
Why it matters: You can start testing without first setting up separate test files.
Key terms: tests, separate test files
14.2.1 Just add #[test], and now it’s a test
This section shows how to mark and run Rust tests, what test functions require, and how Rust decides whether a test passes.
- Marking and running tests: Explain how to turn a function into a test and run tests in the Playground or with Cargo.
Why it matters: Using the test command runs the test functions rather than trying to run the program normally. - Test function requirements: Describe where test functions go relative to main() and the restriction on their arguments.
Why it matters: These requirements help you write a test function that will compile and run. - Passing and failing tests: Explain how Rust decides whether a test passes or fails and how assert_eq! can cause failure.
Why it matters: A test needs to panic when the condition it checks is wrong so that Rust reports a failure.
Key terms: #[test], Test, cargo test, main(), test functions, arguments, panic, assert_eq!
14.2.2 What happens when tests fail
This section shows what failed assertions report and how to get more detail from a backtrace when the failure is hard to locate.
- Failed assertion messages: Explain what happens when an `assert_eq!` test compares unequal values and how its failure output helps identify the mismatch.
Why it matters: The failure message gives you the actual values to compare with what you expected. - Backtraces for difficult failures: Describe when to enable `RUST_BACKTRACE`, how to enable it, and how the section suggests finding your code in its output. Distinguish the ordinary backtrace from the more verbose `full` setting.
Why it matters: A backtrace can point you toward your code when the ordinary failure message is not enough.
Key terms: assert_eq!, assert!, panic, left, right, RUST_BACKTRACE, environment variables, backtrace, std::env::set_var(), verbose backtrace
14.2.3 Writing multiple tests
The section shows that Rust can run multiple tests and explains how to organize them in a test-only module.
- Multiple test functions: Explain that you can write several test functions to check ordinary functions, and that each test function still needs its own #[test] attribute.
Why it matters: This lets you check more than one behavior without treating the tests as a single function. - Test-only module setup: Describe how to put tests in their own module using mod and #[cfg(test)], while keeping #[test] on each test and importing the surrounding functions with use super::*;.
Why it matters: This is the test organization the section says Rust programmers often use.
Key terms: #[test], test functions, mod, #[cfg(test)], use super::*;
14.3 Test-driven development
Introduces test-driven development as writing failing tests before code, then keeping those tests to catch problems after later changes.
- Test-driven development cycle: Explain the test-driven development cycle: write tests that initially fail, write code until they pass, and keep the tests to catch problems when the code changes later.
Why it matters: This explains why failing tests are an expected starting point in TDD and why the tests remain useful afterward.
Key terms: test-driven development, TDD, tests
14.3.1 Building a calculator: Starting with the tests
The section starts a test-driven Subtractor by defining expected results, running the tests against a placeholder method, and adding an initial input check.
- Subtractor test cases: Describe the behavior the tests require of the Subtractor, including ordinary subtraction, consecutive minus signs, trailing spaces and minus signs, and invalid input.
Why it matters: The tests give the later implementation concrete behavior to satisfy. - Placeholder method and test feedback: Explain why the first version has an empty Subtractor struct and a math() method returning a placeholder i32, and describe what the initial test run reports.
Why it matters: A runnable placeholder exposes failing requirements without pretending the calculator is finished. - Initial character validation: Describe the first input check added to math() and the result of running the tests after that change.
Why it matters: The input check satisfies one requirement while leaving the actual subtraction for the next step.
Key terms: TDD, Subtractor, math(), i32, test output, OKAY_CHARACTERS, .chars(), .any(), panic
14.3.2 Putting the calculator together
The section builds a tested Subtractor step by step, using failing tests to reveal parsing mistakes and guide fixes.
- Accumulating numbers from input: Describe how the Subtractor collects digits into num_to_parse, parses a completed number as an i32, and updates a running total.
Why it matters: Collecting digits first lets the calculator handle numbers with more than one digit. - Avoiding empty number parsing: Explain how a failing test exposed an attempt to parse an empty num_to_parse and how an emptiness check prevents it.
Why it matters: A failed test can reveal a specific bad assumption in the parsing code. - Alternating subtraction operations: Describe how the Operation enum represents addition or subtraction and switches variants for each minus sign.
Why it matters: Switching between two states handles repeated minus signs without separately counting them. - Processing the final number: Explain why the final collected number was initially ignored and how a final check and shared .do_operation() method fix it.
Why it matters: A parser must handle a number still waiting when it reaches the end of its input. - Test-driven development cycle: Trace the back-and-forth process of writing tests, implementing code to make them pass, and adding tests as new cases come to mind.
Why it matters: Tests provide repeated feedback while the program is being built, not just a final check. - Limits and value of tests: Distinguish passing tests from proof of perfect code, and explain how rerunning tests helps after code changes.
Why it matters: Tests are useful safeguards after changes, even though they cannot check cases nobody included.
Key terms: Subtractor, total, num_to_parse, i32, ParseIntErrors, Operation, Add, Subtract, .do_operation(), TDD
14.4 Summary
Where this is used later
- Introducing external crates (chapter 16) · Introduces crates and organizing code for others to use.
- Crates and Cargo.toml (chapter 16) · The earlier introduction to crates provides context for adding an external one.
- Using Cargo and what Rust does while it compiles (chapter 18) · Working with a project on the computer builds on the distinction between project files and code Rust includes.
- cfg! (chapter 21) · Running Rust code as a test.
- Viewing backtraces (chapter 21) · The earlier discussion of backtraces when testing code.
- Accessing environment variables (chapter 18) · That section returns to environment variables, which this section uses to control backtraces.
Concepts explained in this chapter
- testA test function is a Rust function executed by the test framework to check program behavior and normally takes no arguments.
- test-driven developmentTest-driven development is a software development technique in which tests are written before implementation code and guide the implementation toward passing behavior.
- moduleA module is a distinct, bounded software unit that groups related code or functionality and provides an organizational or interaction boundary within a larger system.
- public visibilityVisibility is the Rust access-control property that determines whether an item or field can be referenced from a given module or crate.
- attributeA struct field is a named or positional component of a Rust struct whose visibility can be declared independently of the struct type.
- crateAn external crate is a Rust compilation unit published outside the current project for other programs to depend on.
- anyAny is a Rust standard-library trait for values that can support runtime type inspection and downcasting when they satisfy the required lifetime conditions.
- const qualifierA language feature that marks a value, binding, or access path as constant, restricting reassignment or modification according to the language’s rules.
- panicA panic is Rust's unrecoverable failure behavior that stops ordinary execution and reports diagnostic information.
- assert_eqassert_eq! is a Rust macro that panics when two expressions evaluate to unequal values and reports both values.
- environment variableAn environment variable is a named operating-system or process setting represented as a string key and string value.
- stack traceA backtrace is a record of the call stack showing the sequence of function calls leading to a program point or failure.
- conditional compilationA conditional compilation attribute instructs the Rust compiler to include or exclude an item according to a configuration condition.
- DefaultDefault is the Rust standard trait that defines how to construct a default value for a type.
- runtime logic errorA logic bug is a defect in a program's intended behavior that does not necessarily prevent the code from running.
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.
