DEV Community

SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 11.2. Assertions

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

11.2.1 Using the assert! Macro to Check Test Results

The assert! macro comes from the standard library and is used to determine whether a condition is true. It accepts an expression whose return type is boolean:

  • When the value inside assert! is true, the test passes and assert! does nothing extra.
  • When the value inside assert! is false, assert! calls panic!, and the test fails.

For example:

#[derive(Debug)]  
struct Rectangle {    
    width: u32,  
    height: u32,  
}  

impl Rectangle {    
    fn can_hold(&self, other: &Rectangle) -> bool {        
        self.width > other.width && self.height > other.height  
    }
}  
Enter fullscreen mode Exit fullscreen mode

The Rectangle struct stores the width and height of a rectangle. It defines a can_hold method to determine whether one rectangle can fit inside another rectangle, ignoring diagonal placement. The logic is easy to understand: just check whether both the width and height of the current rectangle are greater than those of the other rectangle.

How should we test this method? Since its return type is exactly bool, assert! is a perfect fit:

#[derive(Debug)]  
struct Rectangle {    
    width: u32,  
    height: u32,  
}  

impl Rectangle {    
    fn can_hold(&self, other: &Rectangle) -> bool {        
        self.width > other.width && self.height > other.height  
    }
}  

#[cfg(test)]  
mod tests {  
    use super::*;  
    #[test]  
    fn larger_can_hold_smaller() {  
        let larger = Rectangle {            
            width: 8,  
            height: 7,  
        };        
        let smaller = Rectangle {            
            width: 5,  
            height: 1,  
        };  
        assert!(larger.can_hold(&smaller));  
    }}  
Enter fullscreen mode Exit fullscreen mode

Since test is a module, if code inside the test module wants to use items from outside, it must first import them into the current scope. Here, we write use super::*;, and * imports everything from the outer module into the test module. For more details on this part, see 7.2. Path Pt. 1 and 7.3. Path Pt. 2.

Then look at the test function below. First, it declares two rectangles, larger and smaller, which store the width and height of the large rectangle and the small rectangle respectively. That is the Arrange step.

The assert! macro below calls can_hold, which is the Act step.

Finally, assert! is used to determine whether the test succeeds.

In this example, the width and height stored in larger can definitely contain smaller, so the result must be true, and the test passes.

Run cargo test:

$ cargo test  
   Compiling rectangle v0.1.0 (file:///projects/rectangle)    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)  
running 1 test  
test tests::larger_can_hold_smaller ... ok  

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s  

   Doc-tests rectangle  
running 0 tests  

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s  
Enter fullscreen mode Exit fullscreen mode

What if the smaller rectangle cannot hold the larger one?

#[cfg(test)]  
mod tests {  
    use super::*;  
    #[test]  
    fn larger_can_hold_smaller() {  
        //...    
    }  
    #[test]  
    fn smaller_cannot_hold_larger() {  
        let larger = Rectangle {            
            width: 8,            
            height: 7,        
        };        
        let smaller = Rectangle {            
            width: 5,            
            height: 1,        
        };  
        assert!(!smaller.can_hold(&larger));  
    }}  
Enter fullscreen mode Exit fullscreen mode

Another test function, smaller_cannot_hold_larger, is declared. smaller.can_hold(&larger) must return false, but a negation operator ! is placed in front of it, so the final value received by assert! is still true, and the test passes:

$ cargo test  
   Compiling rectangle v0.1.0 (file:///projects/rectangle)    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)  
running 2 tests  
test tests::larger_can_hold_smaller ... ok  
test tests::smaller_cannot_hold_larger ... ok  

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s  

   Doc-tests rectangle  
running 0 tests  

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s  
Enter fullscreen mode Exit fullscreen mode

Both tests pass, which means the can_hold method is probably fine.

Now change the method so that the width comparison in can_hold uses < instead of >:

#[derive(Debug)]  
struct Rectangle {    
    width: u32,  
    height: u32,  
}  

impl Rectangle {    
    fn can_hold(&self, other: &Rectangle) -> bool {        
        self.width < other.width && self.height > other.height  
    }
}  
Enter fullscreen mode Exit fullscreen mode

The logic is now wrong. Run the same test functions:

$ cargo test  
   Compiling rectangle v0.1.0 (file:///projects/rectangle)    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)  
running 2 tests  
test tests::larger_can_hold_smaller ... FAILED  
test tests::smaller_cannot_hold_larger ... ok  

failures:  

---- tests::larger_can_hold_smaller stdout ----  
thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:  
assertion failed: larger.can_hold(&smaller)  
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace  


failures:  
    tests::larger_can_hold_smaller  
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s  

error: test failed, to rerun pass `--lib`  
Enter fullscreen mode Exit fullscreen mode

One test failed, which means the error was successfully caught. That is also the purpose of writing tests: to discover problems as early as possible.

11.2.2 Testing Equality with assert_eq! and assert_ne!

The eq in assert_eq! stands for equal, and the ne in assert_ne! stands for not equal. Both come from the standard library.

These two macros take two arguments and determine whether the two values are equal. Usually, you pass the result of the code under test as one argument and the expected result as the other argument, and then the macros check whether the two results are equal.

In practice, these macros work a lot like the == and != operators. The difference is that if they fail, they automatically print the values of both arguments, which helps the developer understand why the test failed.

There are some requirements for using these macros. They print values in debug format, so the arguments must implement the PartialEq and Debug traits. All primitive types and most standard library types already do, but custom structs and enums must implement these traits themselves.

An example of implementing the Display trait:

use std::fmt;  

// Define a struct  
struct Point {  
    x: i32,    
    y: i32,
}  

// Implement Display for Point  
impl fmt::Display for Point {  
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {        
        // Define the formatted output        
        write!(f, "Point(x: {}, y: {})", self.x, self.y)    
    }
}  

fn main() {  
    let p = Point { x: 10, y: 20 };    
    println!("{}", p); // Format output using Display
}  
Enter fullscreen mode Exit fullscreen mode

Here is an example using assert_eq!:

pub fn add_two(a: usize) -> usize {  
    a + 2
}  

#[cfg(test)]  
mod tests {  
    use super::*;  
    #[test]    
    fn it_adds_two() {        
        let result = add_two(2);        
        assert_eq!(result, 4);    
    }
}  
Enter fullscreen mode Exit fullscreen mode

Now 4 and 5 are not equal, so changing the assert_eq! in the test function to assert_ne! will make the test pass.

The add_two function adds 2 to its argument. The test function it_adds_two calls add_two; since 2 + 2 = 4, the expected value of add_two(2) is 4, so you just put 4 and the function call into the macro. In Rust, the expected value and the function call can actually be swapped. Some languages require a specific order, but Rust does not. The value on the left side (the first argument) is simply called the left value, and the other is called the right value.

Output:

$ cargo test  
   Compiling adder v0.1.0 (file:///projects/adder)    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)  
running 1 test  
test tests::it_adds_two ... ok  

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s  

   Doc-tests adder  
running 0 tests  

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s  
Enter fullscreen mode Exit fullscreen mode

Next we introduce a logic bug by changing add_two from a + 2 to a + 3, leaving everything else unchanged, and see what happens:

pub fn add_two(a: usize) -> usize {  
    a + 3
}  

#[cfg(test)]  
mod tests {  
    use super::*;  
    #[test]    
    fn it_adds_two() {        
        let result = add_two(2);        
        assert_eq!(result, 4);    
    }
}  

Enter fullscreen mode Exit fullscreen mode

Now 4 and 5 are not equal, so changing the assert_eq! in the test function to assert_ne! will make the test pass.

Top comments (0)