Testing

Unit tests, integration tests, and doctests, plus fakes for time and collaborators so a test does not wait on the world.

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

LayerWhere it livesWhat it can see
UnitThe same file, behind cfg(test)Private functions
Integrationtests/Only the public API of a library crate
DocA code fence in a doc commentcargo test runs it. If the example is wrong, the docs are wrong.
unitprivate OKintegrationpublic onlydoctestin the docs
StatusStart next to the function

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.

01_unit_integration_doctest.rsRust
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.

Servicegeneric clockFixedClocktest double
StatusThe service asks for the time

It does not know whether the clock is the operating system or a constant. That is the same generic bound you already wrote.

02_test_doubles_and_time.rsRust
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));
}