Testing
Testing is built into Rust and Cargo, and needs no extra library: cargo test compiles your code in test mode and runs the test program it produces (the Book). Unit tests live next to your code, integration tests live in a tests/ folder, doc tests live in your comments, and cargo test runs them all.
RememberMark a function with #[test] and check with assert_eq!. cargo test does the rest.
temperature/ ├── src/ │ └── lib.rs the code, plus unit tests at the bottom ├── tests/ │ └── round_trip.rs an integration test: uses the crate from outside ├── Cargo.lock created by Cargo └── Cargo.toml dependencies go here
/// Convert Celsius to Fahrenheit.////// A code block in a doc comment is a doc test: `cargo test` compiles and runs it.////// ```/// assert_eq!(temperature::to_fahrenheit(0.0), 32.0);/// ```pub fn to_fahrenheit(celsius: f64) -> f64 { celsius * 9.0 / 5.0 + 32.0}
/// Parse text like "21C" or "70F" into Celsius.////// ```/// use temperature::parse_celsius;////// assert_eq!(parse_celsius("70F"), Ok(21.11111111111111));/// assert!(parse_celsius("hot").is_err());/// ```pub fn parse_celsius(text: &str) -> Result<f64, String> { // The unit is the last character, not the last byte: "21°" must be an error, // not a panic from slicing through the middle of a two-byte character. let Some(unit) = text.chars().last() else { return Err("empty input".to_string()); }; let number = &text[..text.len() - unit.len_utf8()]; let value: f64 = number .parse() .map_err(|_| format!("not a number: {number:?}"))?; match unit { 'C' => Ok(value), 'F' => Ok((value - 32.0) * 5.0 / 9.0), _ => Err(format!("unknown unit: {unit:?}")), }}
// Unit tests live next to the code. #[cfg(test)] leaves them out of normal builds.#[cfg(test)]mod tests { use super::*;
#[test] fn boiling_point() { assert_eq!(to_fahrenheit(100.0), 212.0); }
#[test] fn parses_fahrenheit() { let parsed = parse_celsius("212F"); println!("212F parsed to {parsed:?}"); assert_eq!(parsed, Ok(100.0)); }
#[test] fn rejects_unknown_unit() { assert!(parse_celsius("20K").is_err()); assert!(parse_celsius("21°").is_err()); }}// Integration tests use the crate from the outside, like any other user would.use temperature::{parse_celsius, to_fahrenheit};
#[test]fn round_trip() { let celsius = parse_celsius("68F").unwrap(); assert_eq!(to_fahrenheit(celsius), 68.0);} Compiling temperature v0.1.0 (/home/you/temperature)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.11s
Running unittests src/lib.rs (target/debug/deps/temperature-dbd6a6154de66132)
running 3 tests
test tests::boiling_point ... ok
test tests::parses_fahrenheit ... ok
test tests::rejects_unknown_unit ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/round_trip.rs (target/debug/deps/round_trip-52cb981126fb5eb5)
running 1 test
test round_trip ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests temperature
running 2 tests
test src/lib.rs - parse_celsius (line 14) ... ok
test src/lib.rs - to_fahrenheit (line 5) ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
all doctests ran in 0.14s; merged doctests compilation took 0.14sCargo ran three kinds of test, one block each: the unit tests in src/lib.rs, the integration test in tests/round_trip.rs, and the two doc tests from the /// comments.
Three things in test output change, on this page and on yours. The hex after each file name (temperature-dbd6a6154de66132) was the same in every run I made here, but it may differ between machines and toolchains. The times (finished in 0.00s) and the thread number in a panic message differ from run to run. Also, the tests of one test program run in parallel threads by default (the Book), so their lines can come out in a different order each time. I measured it: 25 runs of plain cargo test on this project gave 5 distinct outputs (ignoring the times), and 25 runs with -- --test-threads=1 gave 1. That is one machine, not a guarantee. Everything after -- goes to the test program, and -- --test-threads=1 runs one test at a time. Every cargo test on this page uses it where there are several test lines, so the order is the same every run.
The same tests in Go
Section titled “The same tests in Go”package temperature
import ( "fmt" "strconv")
// ToFahrenheit converts Celsius to Fahrenheit.func ToFahrenheit(c float64) float64 { return c*9/5 + 32 }
// ParseCelsius parses text like "21C" or "70F" into Celsius.func ParseCelsius(text string) (float64, error) { if len(text) < 2 { return 0, fmt.Errorf("too short: %q", text) } num, unit := text[:len(text)-1], text[len(text)-1:] v, err := strconv.ParseFloat(num, 64) if err != nil { return 0, fmt.Errorf("not a number: %q", num) } switch unit { case "C": return v, nil case "F": return (v - 32) * 5 / 9, nil } return 0, fmt.Errorf("unknown unit: %q", unit)}package temperature
import "testing"
func TestBoilingPoint(t *testing.T) { if got := ToFahrenheit(100); got != 212 { t.Errorf("got %v, want 212", got) }}
func TestParsesFahrenheit(t *testing.T) { got, err := ParseCelsius("212F") if err != nil || got != 100 { t.Errorf("got %v, %v; want 100, nil", got, err) }}
func TestRejectsUnknownUnit(t *testing.T) { if _, err := ParseCelsius("20K"); err == nil { t.Error("expected an error") }}=== RUN TestBoilingPoint --- PASS: TestBoilingPoint (0.00s) === RUN TestParsesFahrenheit --- PASS: TestParsesFahrenheit (0.00s) === RUN TestRejectsUnknownUnit --- PASS: TestRejectsUnknownUnit (0.00s) PASS ok example.com/temperature 0.001s
Go prints timings too (0.00s, 0.001s), and they vary from run to run.
- Unit tests sit in a
mod testsinside the same file, souse super::*gives them the module’s private functions too (the Book).#[cfg(test)]means that module is compiled bycargo test, not bycargo build. - Integration tests in
tests/use your crate like any outside user would, through its public API. Each file there is its own crate, so it needs its ownuse temperature::.... They need a library (src/lib.rs): a crate that only hassrc/main.rscannot be imported (the Book). - Doc tests: the code blocks in the
///comments of your library are compiled and run bycargo test(Cargo Book), so an example that stops working shows up as a failing test. Each block behaves like its own small program that uses your crate from outside, which is why the second example starts withuse temperature::parse_celsius;. - Extras:
proptestfor property-based testing,instafor snapshot tests,mockallfor mocks. Crates used only by tests go under[dev-dependencies]inCargo.toml: they are compiled for tests, examples and benchmarks, not for a normal build (Cargo Book). This page does not run those three crates.
More than assert_eq!
Section titled “More than assert_eq!”A separate small crate, kelvin, shows the other attributes. This is a different project from the one above, so it has its own test run:
/// Kelvin from Celsius. Panics below absolute zero.pub fn to_kelvin(celsius: f64) -> f64 { assert!(celsius >= -273.15, "below absolute zero: {celsius}"); celsius + 273.15}
#[cfg(test)]mod tests { use super::*;
// Passes only if the code panics with a message containing this text. #[test] #[should_panic(expected = "below absolute zero")] fn rejects_below_absolute_zero() { to_kelvin(-300.0); }
// A test may return a Result, so `?` works inside it. Err fails the test. #[test] fn freezing_point() -> Result<(), String> { let text = "0"; let celsius: f64 = text.parse().map_err(|e| format!("{e}"))?; assert_eq!(to_kelvin(celsius), 273.15); Ok(()) }
// Skipped by a plain `cargo test`; `cargo test -- --ignored` runs only these. #[test] #[ignore = "pretend this one is slow"] fn absolute_zero() { assert_eq!(to_kelvin(-273.15), 0.0); }
// Floating point: compare with a tolerance unless you know the result is exact. #[test] fn floats_need_a_tolerance() { assert_ne!(0.1 + 0.2, 0.3); assert!((0.1_f64 + 0.2 - 0.3).abs() < 1e-9); }}running 4 tests test tests::absolute_zero ... ignored, pretend this one is slow test tests::floats_need_a_tolerance ... ok test tests::freezing_point ... ok test tests::rejects_below_absolute_zero - should panic ... ok test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests kelvin running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s$ cargo test -- --ignored
running 1 test test tests::absolute_zero ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s Doc-tests kelvin running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
#[should_panic(expected = "...")]passes only if the code panics, and the panic message contains that text.- A test can return
Result<(), E>, so?works inside it, and anErrfails the test. You cannot combine this with#[should_panic](the Book). #[ignore]leaves a test out of a normal run, andcargo test -- --ignoredruns only those, as the second command shows.- Floating-point sums are not always exact: the last test asserts that
0.1 + 0.2is not0.3here and then compares with a tolerance. The unit and integration tests above compare results that are exact (212.0,100.0,68.0). The doc test compares190/9with the literal21.11111111111111: that passes here because the literal rounds to the samef64, but it is fragile, and a tolerance is safer for results like that.
Try it yourself
Section titled “Try it yourself”-
Run a subset.
cargo test boilingruns the tests whose name containsboiling, and says how many it skipped (2 filtered out).--libruns only the unit tests and--doconly the doc tests (cargo test). The test names are matched as substrings (libtest options):terminal$ cargo test boiling Compiling temperature v0.1.0 (/home/you/temperature) Finished `test` profile [unoptimized + debuginfo] target(s) in 0.09s Running unittests src/lib.rs (target/debug/deps/temperature-dbd6a6154de66132) running 1 test test tests::boiling_point ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s Running tests/round_trip.rs (target/debug/deps/round_trip-52cb981126fb5eb5) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s $ cargo test --doc Doc-tests temperature running 2 tests test src/lib.rs - parse_celsius (line 14) ... ok test src/lib.rs - to_fahrenheit (line 5) ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s all doctests ran in 0.12s; merged doctests compilation took 0.12s
-
See what a test prints. The
parses_fahrenheittest above has aprintln!("212F parsed to {parsed:?}");. A passing test’s output is captured and not shown.--no-capturelets it print as it happens;--show-outputprints the output of passing tests after all the tests have run (in asuccesses:block). The rustc book lists--nocaptureas a deprecated alias of--no-capture; the last command below ran it in this image and it printed the same line, with no warning:terminal$ cargo test --lib parses running 1 test test tests::parses_fahrenheit ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s $ cargo test --lib parses -- --no-capture running 1 test 212F parsed to Ok(100.0) test tests::parses_fahrenheit ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s $ cargo test --lib parses -- --show-output running 1 test test tests::parses_fahrenheit ... ok successes: ---- tests::parses_fahrenheit stdout ---- 212F parsed to Ok(100.0) successes: tests::parses_fahrenheit test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s $ cargo test --lib parses -- --nocapture running 1 test 212F parsed to Ok(100.0) test tests::parses_fahrenheit ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
-
Break it on purpose. Change
+ 32.0to+ 23.0into_fahrenheit, and runcargo test -- --test-threads=1. The failing test prints the panic location and both values:leftis the first argument ofassert_eq!(what the code returned) andrightthe second. Cargo then stops: the integration and doc tests did not run, because without--no-fail-fastCargo exits after the first test program that fails (cargo test). The number in parentheses after the thread name differs every run:$ cargo test -- --test-threads=1Compiling temperature v0.1.0 (/home/you/temperature) Finished `test` profile [unoptimized + debuginfo] target(s) in 0.10s Running unittests src/lib.rs (target/debug/deps/temperature-dbd6a6154de66132) running 3 tests test tests::boiling_point ... FAILED test tests::parses_fahrenheit ... ok test tests::rejects_unknown_unit ... ok failures: ---- tests::boiling_point stdout ---- thread 'tests::boiling_point' (525) panicked at src/lib.rs:44:9: assertion `left == right` failed left: 203.0 right: 212.0 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::boiling_point test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib`
The same job in your language
Section titled “The same job in your language”| Language | Choices (not a ranking) | Part of the language’s own tools? | Ran here |
|---|---|---|---|
| Rust | the built-in harness, #[test] |
yes: cargo test |
yes (above) |
| Go | the testing package, _test.go files |
yes: go test (docs) |
yes (above) |
| Python | unittest; pytest is a third-party package |
unittest is in the standard library (docs) |
unittest yes, pytest no |
| TypeScript | node:test; vitest, jest |
node:test ships with Node and is marked stable (docs) |
node --test yes (Node 24 ran the .ts file directly, without type-checking it), the others no |
| Java | JUnit | no: a separate framework, and the JDK in the image has no JUnit | no |
| Kotlin | the kotlin.test library on JUnit (docs), or Kotest |
no: you add them in the build file, as the docs’ Gradle example does | no |
| C# | xUnit, NUnit, MSTest | dotnet test is a .NET SDK command that runs tests written with a framework such as xUnit.net or MSTest (docs) |
no |
| C | Unity, CMocka | no test framework: you build a test program and run it. assert.h gives only an assert macro that aborts (man page) |
no |
| C++ | GoogleTest, Catch2 | no: same | no |
For the Java and Kotlin build tools, Gradle’s docs describe a test task that runs the unit tests (docs); I did not run Gradle, Maven, dotnet test or any of the C and C++ frameworks. The image has no test framework for Java, Kotlin, C#, C or C++, so those are not run; Rust, Go, Python (unittest) and Node (node --test) are real runs. Test runners print timings, so the numbers below vary:
The Python and Node runs
import unittest
def to_fahrenheit(celsius): return celsius * 9 / 5 + 32
class TemperatureTests(unittest.TestCase): def test_boiling_point(self): self.assertEqual(to_fahrenheit(100), 212)
def test_freezing_point(self): self.assertEqual(to_fahrenheit(0), 32)
if __name__ == "__main__": unittest.main()import { test } from "node:test";import assert from "node:assert/strict";
function toFahrenheit(celsius: number): number { return (celsius * 9) / 5 + 32;}
test("boiling point", () => { assert.equal(toFahrenheit(100), 212);});
test("freezing point", () => { assert.equal(toFahrenheit(0), 32);});test_boiling_point (test_temperature.TemperatureTests.test_boiling_point) ... ok test_freezing_point (test_temperature.TemperatureTests.test_freezing_point) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK$ node --test temperature.test.ts
✔ boiling point (0.626742ms) ✔ freezing point (0.056083ms) ℹ tests 2 ℹ suites 0 ℹ pass 2 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 43.47771
Read more
Section titled “Read more”- The Rust Book, chapter 11: Writing Automated Tests: how to write tests, controlling how they run, test organization
cargo testin the Cargo Book, and the rustc book’s page on tests (the flags after--)- Documentation tests in the rustdoc book
- Rust by Example: Testing
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.