2 unstable releases

0.1.0 Feb 14, 2021
0.0.0 Oct 13, 2020

#357 in Testing

38 downloads per month

MIT/Apache

42KB
844 lines

chronobreak: Rust mocks for deterministic time testing

crates.io docs.rs build status coverage

chronobreak is a library of test mocks for deterministically testing any time-based property of a given test subject.

Motivation

Let's say we've written a simple function that returns some value after a given timepoint is reached:

use std::time::*;
use std::thread;

fn return_at<T>(time: Instant, t: T) -> T {
    if Instant::now() < time {
        thread::sleep(time.saturating_duration_since(Instant::now()));
    }
    t
}

We now may want to test whether this function actually sleeps as expected:

#[test]
fn test_return_at() {
    let return_time = Instant::now() + Duration::from_secs(1);
    return_at(return_time, 0);
    assert_eq! {Instant::now(), return_time};
}

This test case will most certainly fail. One common strategy to resolve this issue is to expect the time to be within some interval instead of comparing for exact equality. But this will never guarantee that test cases similar to the above will succeed deterministically.

chronobreak to the rescue

So how can we deterministically pass the test?

First, for the mocked clock to work as expected, it is important that for every import for which chronobreak provides a mock, the mock is used when compiling tests:

#[chronobreak]
use std::time::*; // will be replaced with `use chronobreak::mock::std::time::*; for tests
#[chronobreak]
use std::thread;

To make it as easy as possible to not accidentally miss any mock, chronobreak also re-exports all items for the supported libraries that do not require to be mocked.

Now we can test with a mocked clock by simply exchanging #[test] with #[chronobreak::test]:

#[chronobreak::test]
fn test_return_at() {
    let return_time = Instant::now() + Duration::from_secs(1);
    return_at(return_time, 0);
    assert_eq! {Instant::now(), return_time};
}

What happens here is that the mocked version of thread::sleep will act as a decorator for the original function. If the clock is mocked for the current test case, it will advance it by exactly one second. If it is not mocked, thread::sleep will directly delegate to the original function.

The frozen clock

In addition to it's default behaviour of automatically advancing the clock for any timed wait, chronobreak allows freezing the clock. This causes all timed waits to instead block until some other thread advances the clock either manually through clock::advance or clock::advance_to or by performing a timed wait while not being frozen.

This feature is mainly intended to be used in combination with the extended-apis feature which adds Thread::expect_timed_wait and JoinHandle::expect_timed_wait to the public APIs of the mocked versions of those classes. Those functions make it possible to wait for another thread to enter a timed wait before resuming. This is useful for situations where e.g. the test subject is a concurrent data structure and it must be tested that it behaves correctly when it receives input from one thread while it already entered a timed wait on another thread.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in chronobreak by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Dependencies

~8–17MB
~243K SLoC