Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Idiomatic Rust code guidance based on Apollo GraphQL's best practices handbook for ownership, errors, and performance.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/chapter_05.md
1# Chapter 5 - Automated Testing23> Tests are not just for correctness. They are the first place people look to understand how your code works.45* Tests in rust are declared with the attribute macro `#[test]`. Most code editors can compile and run the functions declared under the macro individually or blocks of them.6* Test can have special compilation flags with `#[cfg(test)]`. Also executable in code editors if it contained `#[test]`, it is a good way to mock complicated functions or override traits.78## 5.1 Tests as Living Documentation910In Rust, as in many other languages, tests often show how the functions are meant to be used. If a test is clear and targeted, it's often more helpful than reading the function body, when combined with other tests, they serve as living documentation.1112### Use descriptive names1314> In the unit test name we should see the following:15> * `unit_of_work`: which *function* we are calling. The **action** that will be executed. This is often be the name of the the test `mod` where the function is being tested.16```rust17#[cfg(test)]18mod test {19mod function_name {20#[test]21fn returns_y_when_x() { ... }22}23}24```25> * `expected_behavior`: the set of **assertions** that we need to verify that the test works.26> * `state_that_the_test_will_check`: the general **arrangement**, or setup, of the specific test case.2728#### ❌ Don't use a generic name for a test29```rust30#[test]31fn test_add_happy_path() {32assert_eq!(add(2, 2), 4);33}34```35#### ✅ Use a name which reads like a sentence, describing the desired behavior36> Alternatively, if you function has too many tests, you can blob them together in a `mod`, it makes it easier to read and navigate.3738```rust39// OPTION 140#[test]41fn process_should_return_blob_when_larger_than_b() {42let a = setup_a_to_be_xyz();43let b = Some(2);44let expected = MyExpectedStruct { ... };4546let result = process(a, b).unwrap();4748assert_eq!(result, expected);49}5051// OPTION 252mod process {53#[test]54fn should_return_blob_when_larger_than_b() {55let a = setup_a_to_be_xyz();56let b = Some(2);57let expected = MyExpectedStruct { ... };5859let result = process(a, b).unwrap();6061assert_eq!(result, expected);62}63}64```6566> When executing `cargo test` the test output for each option will look like:67> Option 1: `process_should_return_blob_when_larger_than_b`.68> Option 2: `process::should_return_blob_when_larger_than_b`.6970### Use modules for organization7172Most IDEs can run a single module of tests all together.73The test name in the output will also contain the name of the module.74Together, that means you can use the module name to group related tests together:7576```rust77#[cfg(test)]78mod test { // IDEs will provide a ▶️ button here7980mod process {81#[test] // IDEs will provide a ▶️ button here82fn returns_error_xyz_when_b_is_negative() {83let a = setup_a_to_be_xyz();84let b = Some(-5);85let expected = MyError::Xyz;8687let result = process(a, b).unwrap_err();8889assert_eq!(result, expected);90}9192#[test] // IDEs will provide a ▶️ button here93fn returns_invalid_input_error_when_a_and_b_not_present() {94let a = None;95let b = None;96let expected = MyError::InvalidInput;9798let result = process(a, b).unwrap_err();99100assert_eq!(result, expected);101}102}103}104```105106### Only test one behavior per function107108To keep tests clear, they should describe _one_ thing that the unit does.109This makes it easier to understand why a test is failing.110111#### ❌ Don't test multiple things in the same test112```rust113fn test_thing_parser(...) {114assert!(Thing::parse("abcd").is_ok());115assert!(Thing::parse("ABCD").is_err());116}117```118119#### ✅ Test one thing per test120```rust121#[cfg(test)]122mod test_thing_parser {123#[test]124fn lowercase_letters_are_valid() {125assert!(126Thing::parse("abcd").is_ok(),127// Works like `eprintln`, `format` and `println` macros128"Thing parse error: {:?}",129Thing::parse("abcd").unwrap_err()130);131}132133#[test]134fn capital_letters_are_invalid() {135assert!(Thing::parse("ABCD").is_err());136}137}138```139140> `Ok` scenarios should have an `eprintln` of the `Err` case.141142### Use very few, ideally one, assertion per test143144When there are multiple assertions per test, it's both harder to understand the intended behavior and145often requires many iterations to fix a broken test, as you work through assertions one by one.146147❌ Don't include many assertions in one test:148149```rust150#[test]151fn test_valid_inputs() {152assert!(the_function("a").is_ok());153assert!(the_function("ab").is_ok());154assert!(the_function("ba").is_ok());155assert!(the_function("bab").is_ok());156}157```158159If you are testing separate behaviors, make multiple tests each with descriptive names.160To avoid boilerplate, either use a shared setup function or [rstest](https://crates.io/crates/rstest) cases *with descriptive test names*:161```rust162#[rstest]163#[case::single("a")]164#[case::first_letter("ab")]165#[case::last_letter("ba")]166#[case::in_the_middle("bab")]167fn the_function_accepts_all_strings_with_a(#[case] input: &str) {168assert!(the_function(input).is_ok());169}170```171172> Considerations when using `rstest`173>174> * It's harder for both IDEs and humans to run/locate specific tests.175> * Expectation vs condition naming is now visually inverted (expectation first).176177## 5.2 Add Test Examples to your Docs178179We will deep dive into docs at a later stage, so in this section we will just briefly go over how to add tests to you docs. Rustdoc can turn examples into executable tests using `///` with a few advantages:180181* These tests run with `cargo test` **BUT NOT** `cargo nextest run`. If using `nextest`, make sure to run `cargo t --doc` separately.182* They serve both as documentation and correctness checks, and are kept up to date by changes, due to the fact that the compiler checks them.183* No extra testing boilerplate. You can easily hide test sections by prefixing the line with `#`.184* ❗ There is no issue if you have test duplication between doc-tests and other non-public facing tests.185186```rust187/// Helper function that adds any two numeric values together.188/// This function reasons about which would be the correct type to parse based on the type189/// and the size of the numeric value.190///191/// # Examples192///193/// ```rust194/// # use crate_name::generic_add;195/// use num::numeric;196///197/// # assert_eq!(198/// generic_add(5.2, 4) // => 9.2199/// # , 9.2)200///201/// # assert_eq!(202/// generic_add(2, 2.0) // => 4203/// # , 4)204/// ```205```206207This documentation code would look like:208```rust209use num::numeric;210211generic_add(5.2, 4) // => 9.2212generic_add(2, 2.0) // => 4213```214215## 5.3 Unit Test vs Integration Tests vs Doc tests216217As a general rule, without delving into *test pyramid naming*, rust has 3 sets of tests:218219### Unit Test220221Tests that go in the **same module** as the **tested unit** was declared, this allows the test runner to have visibility over private functions and parent `use` declarations. They can also consume `pub(crate)` functions from other modules if needed. Unit tests can be more focused on **implementation and edge-cases checks**.222223* They should be as simple as possible, testing one state and one behavior of the unit. KISS.224* They should test for errors and edge cases.225* Different tests of the same unit can be combined under a single `#[cfg(test)] mod test_unit_of_work {...}`, allowing multiple submodules for different `units_of_work`.226* Try to keep external states/side effects to your API to minimum and focus those tests on the `mod.rs` files.227* Tests that are not yet fully implemented can be ignored with the `#[ignore = "optional message"]` attribute.228* Tests that intentionally panic should be annotated with the attribute `#[should_panic]`.229230```rust231#[cfg(test)]232mod unit_of_work_tests {233use super::*;234235#[test]236fn unit_state_behavior() {237let expected = ...;238let result = ...;239assert_eq!(result, expected, "Failed because {}", result - expected);240}241}242```243244### Integration Tests245246Tests that go under the `tests/` directory, they are entirely external to your library and use the same code as any other code would use, not have access to private and crate level functions, which means they can **only test** functions on your **public API**.247248> Their purpose is to test whether many parts of the code work together correctly, units of code that work correctly on their own could have problems when integrated.249250* Test for happy paths and common use cases.251* Allow external states and side effects, [testcontainers](https://rust.testcontainers.org/) might help.252* if testing binaries, try to break **executable** and **functions** into `src/main.rs` and `src/lib.rs`, respectively.253254```255├── Cargo.lock256├── Cargo.toml257├── src258│ └── lib.rs259└── tests260├── mod.rs261├── common262│ └── mod.rs263└── integration_test.rs264```265266### Doc Testing267268As mentioned in section [5.2](#52-add-test-examples-to-your-docs), doc tests should have happy paths, general public API usage and more powerful attributes that improve documentation, like custom CSS for the code blocks.269270### Attributes:271272* `ignore`: tells rust to ignore the code, usually not recommended, if you want just a code formatted text, use `text`.273* `should_panic`: tells the rust compiler that this example block will panic.274* `no_run`: compiles but doesn't execute the code, similar to `cargo check`. Very useful when dealing with side-effects for documentation.275* `compile_fail`: Test rustdoc that this block should cause a compilation fail, important when you want to demonstrate wrong use cases.276277## 5.4 How to `assert!`278279Rust comes with 2 macros to make assertions:280* `assert!` for asserting boolean values like `assert!(value.is_ok(), "'value' is not Ok: {value:?}")`281* `assert_eq!` for checking equality between two different values, `assert_eq!(result, expected, "'result' differs from 'expected': {}", result.diff(expected))`.282283### 🚨 `assert!` reminders284* Rust asserts support formatted strings, like the previous examples, those strings will be printed in case of failure, so it is a good practice to add what the actual state was and how it differs from the expected.285* If you don't care about the exact pattern matching value, using `matches!` combined with `assert!` might be a good alternative.286```rust287assert!(matches!(error, MyError::BadInput(_), "Expected `BadInput`, found {error}"));288```289* Use `#[should_panic]` wisely. It should only be used when panic is the desired behavior, prefer result instead of panic.290* There are some other that can enhance your testing experience like:291* [`rstest`](https://crates.io/crates/rstest): fixture based test framework with procedural macros.292* [`pretty_assertions`](https://crates.io/crates/pretty_assertions): overrides `assert_eq` and `assert_ne`, and creates colorful diffs between them.293294## 5.5 Snapshot Testing with `cargo insta`295296> When correctness is visual or structural, snapshots tell the story better than asserts.2972981. Add to your dependencies:299```toml300insta = { version = "1.42.2", features = ["yaml"] }301```302> For most real world applications the recommendation is to use YAML snapshots of serializable values. This is because they look best under version control and the diff viewer and support redaction. To use this enable the yaml feature of insta.3033042. For a better review experience, add the CLI `cargo install cargo-insta`.3053063. Writing a simple test:307```rust308fn split_words(s: &str) -> Vec<&str> {309s.split_whitespace().collect()310}311312#[test]313fn test_split_words() {314let words = split_words("hello from the other side");315insta::assert_yaml_snapshot!(words);316}317```3183194. Run `cargo insta test` to execute, and `cargo insta review` to review conflicts.320321To learn more about `cargo insta` check out its [documentation](https://insta.rs/docs/quickstart/) as it is a very complete and well documented tool.322323### What is snapshot testing?324325Snapshot testing compares your output (text, Json, HTML, YAML, etc) against a saved "golden" version. On future runs, the test fails if the output changes, unless humanly approved. It is perfect for:326* Generate code.327* Serializing complex data.328* Rendered HTML.329* CLI output.330331#### ❌ What not to test with snapshot332* Very stable, numeric-only or small structured data associated logic (prefer `assert_eq!`).333* Critical path logic (prefer precise unit tests).334* Flaky tests, randomly generated output, unless redacted.335* Snapshots of external resources, use mocks and stubs.336337## 5.6 ✅ Snapshot Best Practices338339* Named snapshots, it gives meaningful snapshot files names, e.g. `snapshots/this_is_a_named_snapshot.snap`340```rust341assert_snapshot!("this_is_a_named_snapshot", output);342```343344* Keep snapshots small and clear.345```rust346// ✅ Best case:347assert_snapshot!("app_config/http", whole_app_config.http);348349// ❌ Worst case:350assert_snapshot!("app_config", whole_app_config); // Huge object351```352353> #### 🚨 Avoid snapshotting huge objects354> Huge objects become hard to review and reason about.355356* Avoid snapshotting simple types (primitives, flat enums, small structs):357```rust358// ✅ Better:359assert_eq!(meaning_of_life, 42);360361// ❌ OVERKILL:362assert_snapshot!("the_meaning_of_life", meaning_of_life); // meaning_of_life == 42363```364365* Use [redactions](https://insta.rs/docs/redactions/) for unstable fields (randomly generated, timestamps, uuid, etc):366```rust367use insta::assert_json_snapshot;368369#[test]370fn endpoint_get_user_data() {371let data = http::client.get_user_data();372assert_json_snapshot!(373"endpoints/subroute/get_user_data",374data,375".created_at" => "[timestamp]",376".id" => "[uuid]"377);378}379```380* Commit your snapshots into git. They will be stored in `snapshots/` alongside your tests.381* Review changes carefully before accepting.382