Migrating from assert!
If you have an existing test suite, you do not have to rewrite it all at once.
test-better tests are ordinary #[test] functions; a TestResult-returning
test sits next to a panicking one in the same file. Convert a test when you
next touch it.
This chapter is the translation table.
The shape of the function
A panicking test returns () and its assertions panic. A test-better test
returns TestResult and its assertions are ?-propagated:
#![allow(unused)]
fn main() {
// Before
#[test]
fn before() {
let user = load_user(1);
assert_eq!(user.name, "alice");
}
}
#![allow(unused)]
fn main() {
use test_better::prelude::*;
// After
#[test]
fn after() -> TestResult {
let user = load_user(1);
check!(user.name).satisfies(eq("alice"))
}
}
Assertion translation table
| Panicking | test-better |
|---|---|
assert!(x) | check!(x).satisfies(is_true())? |
assert!(!x) | check!(x).satisfies(is_false())? |
assert_eq!(a, b) | check!(a).satisfies(eq(b))? |
assert_ne!(a, b) | check!(a).satisfies(ne(b))? |
assert!(a < b) | check!(a).satisfies(lt(b))? |
assert!(a >= b) | check!(a).satisfies(ge(b))? |
assert!(v.contains(&x)) | check!(&v).satisfies(contains(eq(x)))? |
assert!(v.is_empty()) | check!(&v).satisfies(is_empty())? |
assert!(s.contains("foo")) | check!(s).satisfies(contains_str("foo"))? |
assert!(opt.is_some()) | check!(opt).satisfies(some(always_matches()))? * |
assert_eq!(opt, Some(x)) | check!(opt).satisfies(some(eq(x)))? |
assert!(res.is_ok()) | check!(res).satisfies(ok(always_matches()))? * |
assert_eq!(res, Ok(x)) | check!(res).satisfies(ok(eq(x)))? |
* some and ok take an inner matcher for the contained value. To assert
only that the option or result is the right variant, pass always_matches();
otherwise pass a matcher for the value you expect inside it.
Replacing .unwrap() and .expect()
.unwrap() and .expect("...") panic. Their ?-friendly replacements live on
the OrFail extension trait, in the prelude:
#![allow(unused)]
fn main() {
use test_better::prelude::*;
fn config_path() -> Option<String> { Some("/etc/app.toml".into()) }
fn read(_: &str) -> Result<String, std::io::Error> { Ok(String::new()) }
#[test]
fn loads_the_config() -> TestResult {
// Before: let path = config_path().unwrap();
let path = config_path().or_fail_with("a config path is configured")?;
// Before: let body = read(&path).expect("config is readable");
let body = read(&path).or_fail_with("the config file is readable")?;
check!(body.is_empty()).satisfies(is_true())
}
}
or_fail()uses a generic message;or_fail_with("...")lets you say what you expected. On aResultit preserves the underlying error as the cause, so the original error message is still in the output.- Use these everywhere you would have reached for
.unwrap()in test setup, not just on the value under test.
Annotating where a failure happened: context
.context("...") (and .with_context(|| ...), which builds its message only
on the failure path) attach a frame describing what the test was doing. They
work on any Result whose error implements std::error::Error, and on a
TestResult directly:
#![allow(unused)]
fn main() {
use test_better::prelude::*;
fn open_db() -> Result<(), std::io::Error> { Ok(()) }
fn run_migrations() -> Result<(), std::io::Error> { Ok(()) }
#[test]
fn the_database_is_ready() -> TestResult {
open_db().context("opening the test database")?;
run_migrations().context("running migrations")?;
Ok(())
}
}
A failure inside run_migrations is reported “while running migrations”, so
you do not have to reconstruct what step you were on from a line number.
A pragmatic order of operations
- Change the signature to
-> TestResultand addOk(())at the end. - Replace each
assert*!with thecheck!form from the table,?on each. - Replace
.unwrap()/.expect()in the test’s setup withor_fail*. - Add
.context(..)where a bare failure would be ambiguous.
The result is a test that, when it fails, tells you what it was doing and what it found, rather than just where the panic was caught.