2 unstable releases
0.1.0 | Feb 14, 2021 |
---|---|
0.0.0 | Oct 13, 2020 |
#367 in Testing
42KB
844 lines
chronobreak: Rust mocks for deterministic time testing
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
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
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
~7–16MB
~238K SLoC