2

I have a method, which has a usage of UTC::now in its implementation...and for sure, this is making an attempt to write a test almost impossible.

The chronos library is being used for the time functionality

I am now basically looking for available mechanisms to mocking/stubbing time in tests in Rust.

Does anyone one have any recommendation on how to handle this situation?

dade
  • 3,340
  • 4
  • 32
  • 53
  • I don't see why it's should be impossible, I see many way to handle it without special trick. For example, accept a range of time instead of exact. – Stargateur Feb 29 '20 at 21:56
  • how can accepting a range be a good idea? – dade Feb 29 '20 at 22:29
  • for example, do a `UTC::now` then call your function then do another `UTC::now`, and check that the returned result is in this range. Without more precision on your use case I don't see the point to continue this thread. There is already question that ask how to mock a lib in rust – Stargateur Feb 29 '20 at 22:31

1 Answers1

-1

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.

metatoaster
  • 17,419
  • 5
  • 55
  • 66
  • Unfortunately, there is a very serious limitation to mocking in this way: `#[cfg(test)]` only works for the _current_ crate under test, and not any of its dependencies. This means that if you have any dependency, you now need to introduce _features_ for each of them, and when your crate is under test, enable the feature (https://stackoverflow.com/questions/27872009/how-do-i-use-a-feature-of-a-dependency-only-for-testing). It's a lot of boilerplate, and it's now possible to accidentally enable the feature outside of tests... – Matthieu M. Jul 28 '23 at 06:52
  • Well, I think I got what you meant about the side-effects and inner calls and dependencies, so to better demonstrate the example I got rid of all the other unrelated distracting cruft (that I lifted from one of my prototype projects), and just have a very simple function instead. – metatoaster Jul 30 '23 at 03:19