Follow these steps to learn more about creating a test suite for your Rust projects:
- Once created, a library project already contains a very simple test (probably to encourage you to write more). The cfg(test) and test attributes tell cargo (the test runner) how to deal with the module:
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
- Before we add further tests, let's add a subject that needs testing. In this case, let's use something interesting: a singly linked list from our other book (Hands-On Data Structures and Algorithms with Rust) made generic. It consists of three parts. First is a node type:
#[derive(Clone)]
struct Node<T> where T: Sized + Clone {
value: T,
next: Link<T>,
}
impl<T> Node<T> where T: Sized + Clone {
fn new(value: T) -> Rc<RefCell<Node<T>>> {
Rc::new(RefCell::new(Node {
value: value,
next: None,
}))
}
}
Second, we have a Link type to make writing easier:
type Link<T> = Option<Rc<RefCell<Node<T>>>>;
The last type is the list complete with functions to add and remove nodes. First, we have the type definition:
#[derive(Clone)]
pub struct List<T> where T: Sized + Clone {
head: Link<T>,
tail: Link<T>,
pub length: usize,
}
In the impl block, we can then specify the operations for the type:
impl<T> List<T> where T: Sized + Clone {
pub fn new_empty() -> List<T> {
List { head: None, tail: None, length: 0 }
}
pub fn append(&mut self, value: T) {
let new = Node::new(value);
match self.tail.take() {
Some(old) => old.borrow_mut().next = Some(new.clone()),
None => self.head = Some(new.clone())
};
self.length += 1;
self.tail = Some(new);
}
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|head| {
if let Some(next) = head.borrow_mut().next.take() {
self.head = Some(next);
} else {
self.tail.take();
}
self.length -= 1;
Rc::try_unwrap(head)
.ok()
.expect("Something is terribly wrong")
.into_inner()
.value
})
}
}
- With the list ready to be tested, let's add some tests for each function, starting with a benchmark:
#[cfg(test)]
mod tests {
use super::*;
extern crate test;
use test::Bencher;
#[bench]
fn bench_list_append(b: &mut Bencher) {
let mut list = List::new_empty();
b.iter(|| {
list.append(10);
});
}
Add some more tests for basic list functionality inside the test module:
#[test]
fn test_list_new_empty() {
let mut list: List<i32> = List::new_empty();
assert_eq!(list.length, 0);
assert_eq!(list.pop(), None);
}
#[test]
fn test_list_append() {
let mut list = List::new_empty();
list.append(1);
list.append(1);
list.append(1);
list.append(1);
list.append(1);
assert_eq!(list.length, 5);
}
#[test]
fn test_list_pop() {
let mut list = List::new_empty();
list.append(1);
list.append(1);
list.append(1);
list.append(1);
list.append(1);
assert_eq!(list.length, 5);
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.length, 0);
assert_eq!(list.pop(), None);
}
}
- It's also a good idea to have an integration test that tests the library from end to end. For that, Rust offers a special folder in the project called tests, which can house additional tests that treat the library as a black box. Create and open the tests/list_integration.rs file to add a test that inserts 10,000 items into our list:
use testing::List;
#[test]
fn test_list_insert_10k_items() {
let mut list = List::new_empty();
for _ in 0..10_000 {
list.append(100);
}
assert_eq!(list.length, 10_000);
}
- Great, now each function has one test. Try it out by running cargo +nightly test in the testing/ root directory. The result should look like this:
$ cargo test
Compiling testing v0.1.0 (Rust-Cookbook/Chapter01/testing)
Finished dev [unoptimized + debuginfo] target(s) in 0.93s
Running target/debug/deps/testing-a0355a7fb781369f
running 4 tests
test tests::test_list_new_empty ... ok
test tests::test_list_pop ... ok
test tests::test_list_append ... ok
test tests::bench_list_append ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/list_integration-77544dc154f309b3
running 1 test
test test_list_insert_10k_items ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests testing
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
- To run the benchmark, issue cargo +nightly bench:
cargo +nightly bench
Compiling testing v0.1.0 (Rust-Cookbook/Chapter01/testing)
Finished release [optimized] target(s) in 0.81s
Running target/release/deps/testing-246b46f1969c54dd
running 4 tests
test tests::test_list_append ... ignored
test tests::test_list_new_empty ... ignored
test tests::test_list_pop ... ignored
test tests::bench_list_append ... bench: 78 ns/iter (+/- 238)
test result: ok. 0 passed; 0 failed; 3 ignored; 1 measured; 0 filtered out
Now, let's go behind the scenes to understand the code better.