By "chronos library" and "usage of UTC::now" I will assume chrono::Utc::now
being the unambiguous method that is being referred to. Claims that "this is making an attempt to write a test almost impossible" is simply untrue, because conditional compilation (relevant thread) may be used to replace the use declaration for a given item with a mocked or faked version that can be better controlled for when running under testing.
As a quick example, this is a function that would return whether or not the provided DateTime
is in the future:
use chrono::{DateTime, Utc};
pub fn in_the_future(dt: DateTime<chrono::Utc>) -> bool {
dt > Utc::now()
}
In order to allow the injection of a mocked version of Utc
, the use
declaration should be modified to ensure that testing gets the mocked/stubbed version, that normal builds get the real thing, the conditional compilation should be used for Utc
, so replace the original with the following:
#[cfg(test)]
use crate::mock_chrono::Utc;
#[cfg(not(test))]
use chrono::Utc;
// the other use will remain unchanged.
use chrono::DateTime;
The above configures the module to use the actual chrono::Utc
when not built for testing, and the mocked version when built as part of testing.
Now to implement the mock itself - first, provide the now()
method with the same signature that would return some value that can be controlled from the test. The module mock_chrono
will take the following form:
use chrono::{DateTime, NaiveDateTime};
use std::cell::Cell;
thread_local! {
static TIMESTAMP: Cell<i64> = const { Cell::new(0) };
}
pub struct Utc;
impl Utc {
pub fn now() -> DateTime<chrono::Utc> {
DateTime::<chrono::Utc>::from_utc(
TIMESTAMP.with(|timestamp| {
NaiveDateTime::from_timestamp_opt(timestamp.get(), 0)
.expect("a valid timestamp set")
}),
chrono::Utc,
)
}
}
pub fn set_timestamp(timestamp: i64) {
TIMESTAMP.with(|ts| ts.set(timestamp));
}
This module exports two symbols - the Utc
that provide a custom now()
method that can return a fully controlled chrono::DateTime<chrono::Utc>
, plus the function set_timestamp
that accepts an i64 offset from the unix epoch (this is done like so for simplicity of the data structure, it can be as complicated as the design requires). Usage in a test may look something like this:
#[test]
fn test_record_past() {
set_timestamp(1357908642);
assert!(!in_the_future(
"2012-12-12T12:12:12Z"
.parse::<DateTime<Utc>>()
.expect("valid timestamp"),
));
}
#[test]
fn test_record_future() {
set_timestamp(1539706824);
assert!(in_the_future(
"2022-02-22T22:22:22Z"
.parse::<DateTime<Utc>>()
.expect("valid timestamp"),
));
}
Complete MVCE (Playground):
/*
[dependencies]
chrono = "0.4.26"
*/
use crate::demo::in_the_future;
pub fn main() {
let dt = chrono::Utc::now();
println!("{dt} is in the future: {}", in_the_future(dt));
}
#[cfg(test)]
mod test {
use crate::demo::in_the_future;
use crate::mock_chrono::set_timestamp;
use chrono::{DateTime, Utc};
#[test]
fn test_record_past() {
set_timestamp(1357908642);
assert!(!in_the_future(
"2012-12-12T12:12:12Z"
.parse::<DateTime<Utc>>()
.expect("valid timestamp"),
));
}
#[test]
fn test_record_future() {
set_timestamp(1539706824);
assert!(in_the_future(
"2022-02-22T22:22:22Z"
.parse::<DateTime<Utc>>()
.expect("valid timestamp"),
));
}
}
#[cfg(test)]
mod mock_chrono {
use chrono::{DateTime, NaiveDateTime};
use std::cell::Cell;
thread_local! {
static TIMESTAMP: Cell<i64> = const { Cell::new(0) };
}
pub struct Utc;
impl Utc {
pub fn now() -> DateTime<chrono::Utc> {
DateTime::<chrono::Utc>::from_utc(
TIMESTAMP.with(|timestamp| {
NaiveDateTime::from_timestamp_opt(timestamp.get(), 0)
.expect("a valid timestamp set")
}),
chrono::Utc,
)
}
}
pub fn set_timestamp(timestamp: i64) {
TIMESTAMP.with(|ts| ts.set(timestamp));
}
}
mod demo {
#[cfg(test)]
use crate::mock_chrono::Utc;
use chrono::DateTime;
#[cfg(not(test))]
use chrono::Utc;
pub fn in_the_future(dt: DateTime<chrono::Utc>) -> bool {
dt > Utc::now()
}
}
One might ask why the need for thread_local!()
, couldn't a more simple static mut
be used for this simple test? Well, while it is possible to create a global mutable singleton and use it, that is very much the wrong approach, as tests normally run concurrently in their own threads. Rather than having every thread trying to wrestle control over a single variable (which either results in every test stamping over each other or creating a bottleneck), giving every thread their thread local own would be the logical sensible choice.
As it turns out, mock_instant
, a library that address a similar need, also use thread locals in a similar manner.