Testing introduction
Cargo provides a test runner, cargo test
which runs functions annotated with #[test]
. Lets create a test which checks that get_db_create_if_missing
does not crash if called twice, src/tests.rs:
# #![allow(unused_variables)] #fn main() { #[cfg(test)] mod database { use crate::*; use std::fs::remove_file; #[test] fn creating_database_twice_should_not_fail() { get_db_create_if_missing("test.sqlite"); get_db_create_if_missing("test.sqlite"); remove_file("test.sqlite").unwrap(); } } #}
Here, #[cfg(test)]
tells Rust that the module should only compile when compiling tests. mod database
is a grouping for the database tests.
Running the test:
~/g/b/sakkosekk $ cargo test
Compiling sakkosekk v0.1.0 (/Users/arve/git/build-your-own-couchdb/sakkosekk)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running target/debug/deps/sakkosekk-5d572632b2e9bfcc
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running 0 tests? We need to add mod tests
to src/main.rs, such that the tests
module is found:
use rusqlite::{named_params, Connection, Error as SqliteError, Row}; use std::path::Path; mod tests; fn main() { let db = get_db_create_if_missing("database.sqlite"); ...
Really run the test:
~/g/b/sakkosekk $ cargo test
Compiling sakkosekk v0.1.0 (/Users/arve/git/build-your-own-couchdb/sakkosekk)
Finished dev [unoptimized + debuginfo] target(s) in 1.85s
Running target/debug/deps/sakkosekk-5d572632b2e9bfcc
running 1 test
test tests::database::creating_database_twice_should_not_fail ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Insertion
Naivly adding an insertion test:
# #![allow(unused_variables)] #fn main() { #[cfg(test)] mod database { use crate::*; use std::fs::remove_file; const TEST_DB_FILENAME: &str = "test.sqlite"; #[test] fn creating_database_twice_should_not_fail() { get_db_create_if_missing(TEST_DB_FILENAME); get_db_create_if_missing(TEST_DB_FILENAME); clean_up(); } #[test] fn insertion() { let db = get_db_create_if_missing(TEST_DB_FILENAME); let document = Document { id: String::from("asdf"), revision: 0, hash: vec![0u8], data: String::from(r#"{ "a": 1, "b": 123 }"#), }; document.insert(&db).expect("Unable to insert document."); clean_up(); } fn clean_up() { remove_file(TEST_DB_FILENAME).unwrap(); } } #}
This will fail:
~/g/b/sakkosekk (master|✚2…) $ cargo test
Compiling sakkosekk v0.1.0 (/Users/arve/git/build-your-own-couchdb/sakkosekk)
Finished dev [unoptimized + debuginfo] target(s) in 1.14s
Running target/debug/deps/sakkosekk-5d572632b2e9bfcc
running 2 tests
test tests::database::insertion ... ok
test tests::database::creating_database_twice_should_not_fail ... FAILED
failures:
---- tests::database::creating_database_twice_should_not_fail stdout ----
thread 'tests::database::creating_database_twice_should_not_fail' panicked at 'Unable to create documents table.: SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some("table documents already exists"))', src/libcore/result.rs:999:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
failures:
tests::database::creating_database_twice_should_not_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--bin sakkosekk'
The test creating_database_twice_should_not_fail
fails, as tests runs in parallel. We'll use a helper function with
that:
- takes a filename and a test function,
- creates a database connection,
- runs the given test function with the created database connection,
- and removes the database file.
Using the function should look like:
# #![allow(unused_variables)] #fn main() { with("filename.sqlite", |db| { // use db connection }); // with will clean up / remove the database #}
with
implementation:
# #![allow(unused_variables)] #fn main() { fn with<F>(filename: &str, test: F) where F: Fn(Connection) -> (), { let db = get_db_create_if_missing(filename); test(db); remove_file(filename).unwrap(); } #}
Note: An alternative with
is to implement Drop
for our own Connection
-type.
The tests rewritten to use with
:
# #![allow(unused_variables)] #fn main() { #[test] fn creating_database_twice_should_not_fail() { with("creating_twice.sqlite", |_| { get_db_create_if_missing("creating_twice.sqlite"); }); } #[test] fn insertion() { with("insertion.sqlite", |db| { let document = Document { id: String::from("asdf"), revision: 0, hash: vec![0u8], data: String::from(r#"{ "a": 1, "b": 123 }"#), }; document.insert(&db).expect("Unable to insert document."); }); } #}
Note that the tests use different filenames for the database.
Running the tests does not fail:
~/g/b/sakkosekk $ cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/debug/deps/sakkosekk-5d572632b2e9bfcc
running 2 tests
test tests::database::creating_database_twice_should_not_fail ... ok
test tests::database::insertion ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Next up: Tests that are expected to fail, like inserting a document with the same identifier twice.