Testing Rust
The two pillars of rust are speed safety Rust built in safety features Borrow checker Type system Pattern matching Borrow checker The borrow checker is an useful feature of the Rust language. It helps to manage ownership. fn main() { let s1:String = String::from("Hello Rust"); let s2: String = s1; println!("string is : {}", s1); } If we run this main then it will provide error like follows: error[E0382]: borrow of moved value: `s1` --> src\main.rs:5:32 | 2 | let s1:String = String::from("Hello Rust"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait 3 | let s2: String = s1; | -- value moved here 4 | 5 | println!("string is : {}", s1); | ^^ value borrowed here after move | Type system Rust type system is designed to guarantee safety and performance in your programs. Rust's type system is one of its most powerful features, providing safety, expressiveness, and performance in your programs. Core characteristics of rust are: - Statically typed: All types are known at compile time. - Strongly typed: No implicit type conversions. - Type inference: Compiler can often deduce types. - Zero-cost abstractions: High-level constructs compile to efficient low-level code. Basic types of rust are: Scalar Types Integers: i8, i16, i32, i64, i128, isize (signed); u8, u16, u32, u64, u128, usize (unsigned) Floats: f32, f64 Boolean: bool (true/false) Character: char Compound Types Tuples: Fixed-size, heterogeneous collections Arrays: Fixed-size, homogeneous collections Custom Types Structs Enums Pattern matching Pattern matching enables you to match and destructure data according to its type, structure, or other attributes. It offers a clear and expressive approach to handling various situations and reaching conclusions in light of them. Because of Rust's thorough pattern matching, you must take into consideration every potential pattern to guarantee that your code is comprehensive and error-free. Rust's Standard Library gives us three assertion macros that helps us build our testing scenarios. assert! - evaluates a specific condition, which passes the test if that condition is true, and triggers a failure with a panic if that condition is false. assert_eq! - compares two values for equality, passing the test if they're equal and detailing the discrepancy if they're not. assert_ne! - checks for inequality between two values, passing the test if they are unequal and detailing if they are equal. pub fn add(left: u64, right: u64) -> u64 { left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result = add(2, 2); assert_eq!(result, 4); } } we can run test by cargo test. The it will give output as follows: Compiling rust_testing v0.1.0 (C:\Users\Sanija Methsen\Desktop\Rust\my-projects\rust_testing) Finished `test` profile [unoptimized + debuginfo] target(s) in 3.32s Running unittests src\lib.rs (target\debug\deps\rust_testing-7cd7699070227f78.exe) running 1 test test tests::it_works ... ok successes: successes: tests::it_works test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s if the answer is not correct then it'll display failed message in terminal pub fn add(left: u64, right: u64) -> u64 { left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result = add(10, 2); assert_eq!(result, 4); } } ---- tests::it_works stdout ---- thread 'tests::it_works' panicked at src\lib.rs:12:9: assertion `left == right` failed left: 12 right: 4 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_works test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib Rust have the option to tag along a custom message with the failure alert whenever you use the assert!, the assert_eq!, or the assert_ne! macros. pub fn sum(left: u64, right: u64) -> u64 { left + right } pub fn subtract(left: u64, right: u64) -> u64 { left - right } pub fn is_even(num:i32) -> bool { num % 2 == 0 } #[cfg(test)] mod tests { use super::*; #[test] fn test_sum() { assert_eq!(4, sum(2, 2), "calculating sum failed"); } #[test] fn test_subtract() { assert_eq!(4, subtract(6, 2), "calculating subtraction failed"); } #[test] fn test_is_even() { assert!(is_even(3), "number is not even"); } } output: PS C:\Users\Sanija Methsen\Desktop\Rust\my-projects\rust_testing> cargo test Compiling rust_testing v0.1.0 (C:\Users\Sanija Methsen\Desktop\Rust\my-pr

The two pillars of rust are
- speed
- safety
Rust built in safety features
- Borrow checker
- Type system
- Pattern matching
Borrow checker
The borrow checker is an useful feature of the Rust language. It helps to manage ownership.
fn main() {
let s1:String = String::from("Hello Rust");
let s2: String = s1;
println!("string is : {}", s1);
}
If we run this main then it will provide error like follows:
error[E0382]: borrow of moved value: `s1`
--> src\main.rs:5:32
|
2 | let s1:String = String::from("Hello Rust");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2: String = s1;
| -- value moved here
4 |
5 | println!("string is : {}", s1);
| ^^ value borrowed here after move
|
Type system
Rust type system is designed to guarantee safety and performance in your programs.
Rust's type system is one of its most powerful features, providing safety, expressiveness, and performance in your programs. Core characteristics of rust are:
- Statically typed: All types are known at compile time.
- Strongly typed: No implicit type conversions.
- Type inference: Compiler can often deduce types.
- Zero-cost abstractions: High-level constructs compile to
efficient low-level code.
Basic types of rust are:
-
Scalar Types
- Integers: i8, i16, i32, i64, i128, isize (signed); u8, u16, u32, u64, u128, usize (unsigned)
- Floats: f32, f64
- Boolean: bool (true/false)
- Character: char
-
Compound Types
- Tuples: Fixed-size, heterogeneous collections
- Arrays: Fixed-size, homogeneous collections
-
Custom Types
- Structs
- Enums
Pattern matching
Pattern matching enables you to match and destructure data according to its type, structure, or other attributes. It offers a clear and expressive approach to handling various situations and reaching conclusions in light of them. Because of Rust's thorough pattern matching, you must take into consideration every potential pattern to guarantee that your code is comprehensive and error-free.
Rust's Standard Library gives us three assertion macros that helps us build our testing scenarios.
- assert! - evaluates a specific condition, which passes the test if that condition is true, and triggers a failure with a panic if that condition is false.
- assert_eq! - compares two values for equality, passing the test if they're equal and detailing the discrepancy if they're not.
- assert_ne! - checks for inequality between two values, passing the test if they are unequal and detailing if they are equal.
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
we can run test by cargo test
. The it will give output as follows:
Compiling rust_testing v0.1.0 (C:\Users\Sanija Methsen\Desktop\Rust\my-projects\rust_testing)
Finished `test` profile [unoptimized + debuginfo] target(s) in 3.32s
Running unittests src\lib.rs (target\debug\deps\rust_testing-7cd7699070227f78.exe)
running 1 test
test tests::it_works ... ok
successes:
successes:
tests::it_works
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
if the answer is not correct then it'll display failed message in terminal
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(10, 2);
assert_eq!(result, 4);
}
}
---- tests::it_works stdout ----
thread 'tests::it_works' panicked at src\lib.rs:12:9:
assertion `left == right` failed
left: 12
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_works
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib
Rust have the option to tag along a custom message with the failure alert whenever you use the assert!, the assert_eq!, or the assert_ne! macros.
pub fn sum(left: u64, right: u64) -> u64 {
left + right
}
pub fn subtract(left: u64, right: u64) -> u64 {
left - right
}
pub fn is_even(num:i32) -> bool {
num % 2 == 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(4, sum(2, 2), "calculating sum failed");
}
#[test]
fn test_subtract() {
assert_eq!(4, subtract(6, 2), "calculating subtraction failed");
}
#[test]
fn test_is_even() {
assert!(is_even(3), "number is not even");
}
}
output:
PS C:\Users\Sanija Methsen\Desktop\Rust\my-projects\rust_testing> cargo test
Compiling rust_testing v0.1.0 (C:\Users\Sanija Methsen\Desktop\Rust\my-projects\rust_testing)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.64s
Running unittests src\lib.rs (target\debug\deps\rust_testing-7cd7699070227f78.exe)
running 3 tests
test tests::test_is_even ... FAILED
test tests::test_subtract ... FAILED
test tests::test_sum ... FAILED
failures:
---- tests::test_is_even stdout ----
thread 'tests::test_is_even' panicked at src\lib.rs:30:9:
number is not even
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
---- tests::test_subtract stdout ----
thread 'tests::test_subtract' panicked at src\lib.rs:25:9:
assertion `left == right` failed: calculating subtraction failed
left: 4
right: 3
---- tests::test_sum stdout ----
thread 'tests::test_sum' panicked at src\lib.rs:20:9:
assertion `left == right` failed: calculating sum failed
left: 4
right: 5
failures:
tests::test_is_even
tests::test_subtract
tests::test_sum
test result: FAILED. 0 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
There is a anothor marco for testing. That is panic!
. This verifies that function panics as expected during testing, and checks that the panic message matches specific text.
pub fn divide(num1:i32, num2:i32) -> i32 {
if num2 == 0 {
panic!("divide by zero not allowed");
}
num1 / num2
}
#[test]
#[should_panic(expected="Divide by zero not allowed")]
fn test_check_divide() {
divide(10, 0);
}
running 1 test
test tests::test_check_divide - should panic ... ok
if #[should_panic(expected="Divide by zero not allowed")]
not included to test function then it will throw an error. If we use
#[test]
#[should_panic(expected="Divide by zero not allowed")]
fn test_check_divide() {
divide(10, 2);
}
then test will be fail.
running 1 test
test tests::test_check_divide - should panic ... FAILED
Result type
Using result type in testing is one clever way to handle success and failure events in tests.
pub fn is_even(num:i32) -> bool {
num % 2 == 0
}
#[test]
fn test_even_number() -> Result<(), String> {
if is_even(4) {
Ok(())
} else {
Err(String::from("Expected 4 to be even"))
}
}
output:
running 1 test
test tests::test_even_number ... ok
successes:
successes:
tests::test_even_number
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Test Attributes
#[test]
- Indicates a function is a test to be run. This function takes no arguments.
#[bench]
- Indicates a function is a benchmark to be run. This function takes one argument (test::Bencher).
#[should_panic]
- This function (also labeled with #[test]
) will only pass if the code causes a panic (an assertion failure or panic!) A message may be provided, which the failure string must contain:
#[should_panic(expected = "foo")]
.
#[ignore]
- When applied to a function which is already attributed as a test, then the test runner will ignore these tests during normal test runs. Running with --ignored or --include-ignored will run these tests.
Unit tests
Unit tests are essential elements of Rust's testing framework. They are the primary lines of defense against bugs. They're quick to write, quick to run, and they zero in on the smallest part of our system, our functions and our methods. So their speed and precision make them ideal for very quick iterations, allowing us to confirm that each piece of our code operates exactly as intended. Unit tests are just as important in binary applications as they are in libraries.The standard practice is that you put these test functions in a test module inside each file and you annotate the module with cfg
test. cfg
test attribute is Rust's way of optimizing the testing process. So when we append this attribute to a module, we're telling Rust to compile and execute the code exclusively when we run cargo test not when you run cargo build.
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
Running single test
We can pass the name of any test function to cargo test to run only that test:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2));
}
#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3));
}
#[test]
fn one_hundred() {
assert_eq!(102, add_two(100));
}
}
Run the test:
$ cargo test one_hundred
Filtering to Run Multiple Tests
We can specify part of a test name, and any test whose name matches that value will be run.
$ cargo test add
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-06a75b4a1f2515e9
running 2 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
Integration tests
Integration tests are conducted outside of your library. They can only call functions included in your library's public API since they use it in the same manner as any other code.
extern crate adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
Mocking
Mocking is a practice in unit testing. In mocking, you create fake objects, called mocks, from real objects for testing purposes. These mocks simulate the behavior of the real object as much as it needs to. is this correct.
Reasons for Mocking
- Control over test environment
- Isolation of components for unit testing
- Improved test stability and reliability
- Efficiency and speed
- Enabling testing of unavailable or incomplete components
mockall
crate is most popular mocking library in the Rust ecosystem:
[dependencies]
mockall = "0.11"
use mockall::{automock, predicate::*};
#[automock]
pub trait Adder {
fn add(&self, a: i32, b: i32) -> i32;
}
pub fn add_with_logger(adder: &dyn Adder, a: i32, b: i32) -> i32 {
let result = adder.add(a, b);
println!("Adding {} + {} = {}", a, b, result);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_with_logger() {
let mut mock = MockAdder::new();
mock.expect_add()
.with(eq(2), eq(3))
.returning(|a, b| a + b);
let result = add_with_logger(&mock, 2, 3);
assert_eq!(result, 5);
}
}