Testing
The language, the errors, and the unsafe boundary are in place. Tests lock those contracts. First where a test lives, then how to stop a test depending on the real clock. Flakes and doubles make no sense before you have seen a passing assert.
Unit, integration, and doc tests
| Layer | Where it lives | What it can see |
|---|---|---|
| Unit | The same file, behind cfg(test) | Private functions |
| Integration | tests/ | Only the public API of a library crate |
| Doc | A code fence in a doc comment | cargo test runs it. If the example is wrong, the docs are wrong. |
assert!, assert_eq!, should_panic, and a test that returns Result are the tools. unwrap in a test is allowed. You decided that in the error lesson.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn div(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("zero")
} else {
Ok(a / b)
}
}
fn main() {
assert_eq!(add(1, 2), 3);
assert!(div(4, 0).is_err());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds() {
assert_eq!(add(1, 2), 3);
}
#[test]
#[should_panic]
fn panics_on_purpose() {
assert!(false);
}
#[test]
fn division() -> Result<(), String> {
div(4, 2).map(|_| ()).map_err(|e| e.to_string())
}
}Time and doubles
A test that calls the real clock flakes. Pass a Clock in. A fixed clock returns the instant you chose, so expiry is arithmetic, not a sleep. A fake stands in for a dependency you own. A stub returns canned data. A mock records calls when you must assert the interaction. Prefer a fake of your own trait over a mock of a third-party client. Async tests use a runtime attribute such as tokio::test. Snapshot tests, such as insta, compare a whole string when the value is large and stable.
It does not know whether the clock is the operating system or a constant. That is the same generic bound you already wrote.
trait Clock {
fn now(&self) -> u64;
}
struct FixedClock(u64);
impl Clock for FixedClock {
fn now(&self) -> u64 {
self.0
}
}
struct Service<C: Clock> {
clock: C,
ttl: u64,
}
impl<C: Clock> Service<C> {
fn expired(&self, issued: u64) -> bool {
self.clock.now().saturating_sub(issued) > self.ttl
}
}
fn main() {
let service = Service { clock: FixedClock(1_000), ttl: 30 };
println!("{}", service.expired(900));
}