Expand description

clocks - testable time

This crate provides a way of making code that depends on time testable. In your code, instead of calling out to Utc::now or similar, you instead use Clock::now. When your program is running normally in production, it will behave just the same as without using this crate.

Basic usage

When you need to run tests on your program, you can replace the clock object with a fake clock that lets you control the passage of time. This can be very helpful if you have logic that depends on the passage of time (e.g. if you expect to time things out after minutes or days, and want to test that works).

The mechanism for this is the Clock struct, which can be either a wall clock or a fake clock, and has an associated timezone. Your production code needs to be able to accept a clock so that tests can override the default wall clock.

Example:

use clocks::Clock;
use chrono::Utc;

pub fn production() {
   production_with_clock(Default::default())
}

pub(crate) fn production_with_clock(clock: Clock<Utc>) {
   let start = clock.now();
   // ...
}

#[test]
fn test_basic() {
   let c = Clock::new_fake(Utc::now());
   production_with_clock(c.clone());
}

Sleeping

This crate can also help you test sleep loops, where you want in your test to synchronise with code that sleeps regularly. This is much less hit and miss than injecting sleeps into your test to try to match the sleeping pattern of the code under test, but it can take some effort to get right.

It works only when you have multiple threads or tasks. Give each thread or task that needs to sleep a “split” of the clock using Clock::split. The test thread needs its own split also. Now Clock::advance will wait for all other splits to be blocking in Clock::sleep.

It is easier to make mistakes than would be ideal, but your production code will be unaffected by using this functionality in your tests. Your tests may fail due to errors in usage here, but none of the test code has any effect in production. The most likely error is failing to call Clock::split instead of Clock::clone in some place where you intend to hand the clone to some other thread.

Effectively, we end up needing the clock to be properly split for each thread, and we can neither find out when our clock was transferred to another thread or task, nor is there any equivalent of Send for async code meaning we cannot prevent clones being shared.

Example:

use clocks::Clock;
use chrono::{Duration, Utc};

pub fn production() {
   production_with_clock(Default::default())
}

pub fn production_with_clock(mut clock: Clock<Utc>) {
   let time = clock.now();
   clock.sleep_blocking(Duration::seconds(10));
   assert_eq!(clock.now(), time + Duration::seconds(10));
}

#[test]
fn test_sleep() {
   let mut c = Clock::new_fake(Utc::now());
   let c2 = c.split();
   std::thread_spawn(|| production_with_clock(c2));
   c.advance(Duration::secs(10));
}

Structs

A testable source of time