Getting Started
-
You can create a boilerplate rust project that automatically has tests using the following commmand:
cargo new <project-name> --lib
-
A
test
in Rust is a function that’s annotated with the test attribute. -
The bodies of test functions typically perform these three actions:
- Set up any needed data or state.
- Run the code you want to test.
- Assert the results are what you expect.
-
Two attributes to keep in mind:
#[test]
- To change a function into a test function, add#[test]
on the line beforefn
.#[should_panic]
- To declare before each test function that if this function panics then it is working correctly.
-
After adding the attribute
#[test]
, the rust compiler is ready to runcargo test
. -
When you run the command, behind the scenes Rust builds a test runner binary that runs the functions annotated with the
test
attribute. -
You can write the functions that are not tests inside the tests module, for example a helper function. So, the only way for the Rust to know whether a function is a test function is through the
#[test]
attribute. -
The tests fail when something in the test function panics.
-
Here is the table for the logging statistics:
Statistic Meaning Passed Passing Tests Failed Failing Tests Ignored Tests that were ignored due to #[ignore]
attribute.Measured This is for benchmark tests that measure performance. (only in nightly Rust) Filtered Out While running specific tests, the left out tests are called filtered
. -
Here is the table for the macros you mauy use for assertion:
Assertion Macro Use Case Argument(s) assert!()
If the condition is true then passes else panics. Condition panic!()
Panics or fails the test with a message if given. Message assert_eq!()
Passes if equal else panics. ( ==
)(actual, expected) assert_ne!()
Passes if not equal else panics. ( !=
)(actual, not_expected) -
In rust the convention doesn't matter, we can either use actual as first argument or as second. It is the programmer's convention.
-
In case we are writing the tests in a module inside the same file then we'll need to use the
super::*;
inside the tests module to pull all the outside code of the current file.
#![allow(unused)] fn main() { //Filename: src/lib.rs fn do_something() { ... } #[cfg(test)] mod tests { // The line below will pull all the code of outer module inside. use super::*; #[test] fn test_do_something() { ... } } }
-
For structs and enums that you define, you’ll need to implement
PartialEq
to assert that values of those types are equal or not equal.#![allow(unused)] fn main() { #[derive(PartialEq, Debug)] struct Rectangle { width: u32, height: u32, } #[cfg(test)] mod tests { use super::*; // This test will only work if we'll add #[derive(PartialEq)] to the struct or enum. #[test] fn rectangle_is_of_same_size() { let rectangle1 = Rectangle { width: 8, height: 7, }; let rectangle2 = Rectangle { width: 8, height: 7, }; assert_eq!(rectangle1, rectangle2); } } }
-
You’ll need to implement
Debug
to the struct or enum if you want to see the logs that say(left != right)
.---- tests::rectangle_is_of_same_size stdout ---- thread 'tests::rectangle_is_of_same_size' panicked at 'assertion failed: `(left == right)` left: `Rectangle { width: 8, height: 7 }`, right: `Rectangle { width: 8, height: 8 }`', src/lib.rs:56:9
-
The
assert!()
macro also allows the message to show in case the test fails.#![allow(unused)] fn main() { assert!( result.contains("something"), "The result doesn't contain something. This was the actual result: {}", result ) }
-
There is an attribute named
#[should_panic]
, that you can write before any test function to declare that if this function panics then it is working correctly.#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100() { Guess::new(200); } } }
-
This is the note that appears in case the function doesn't panics.
note: test did not panic as expected
-
To make the
![should_panic]
attribute more precise we can add theexpected
parameter and pass a string to it such that the string is a substring of the relevant panic message.#![allow(unused)] fn main() { impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!( "Guess value must be greater than or equal to 1, got {}.", value ); } else if value > 100 { panic!( "Guess value must be less than or equal to 100, got {}.", value ); } Guess { value } } } #[cfg(test)] mod tests { use super::*; // The below test only passes if both the two conditions satisfies: // 1. Code should panic // 2. The string passed in expected parameter is a substring of the panic message. #[test] #[should_panic(expected = "Guess value must be less than or equal to 100")] fn greater_than_100() { Guess::new(200); } } }
-
There is also an alternative approach possible to use
Ok()
andErr()
inside a test.#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not equal four")) } } } }
-
Pros: The only upside of writing tests such that they return a
Result<T, E>
enables you to use the question mark (?
) operator in the body of tests, which can be a convenient way to write tests that should fail if any operation within them returns an Err variant. -
Cons: If we write tests in above manner than we cannot use
#[should_panic]
attribute because we can useErr()
.