如何在Rust测试中模拟时间

3

我有一个方法,在其实现中使用了UTC::now,这肯定会使测试变得几乎不可能。

时间功能正在使用chronos库。

现在我基本上正在寻找可用的机制来模拟/存根Rust测试中的时间。

有人对如何处理这种情况有任何建议吗?


我不明白为什么这是不可能的,我看到很多处理它的方法,而不需要特殊的技巧。例如,接受一个时间范围而不是具体的时间。 - undefined
接受一个范围是个好主意吗? - undefined
例如,执行UTC::now然后调用您的函数,然后再次执行UTC::now,并检查返回的结果是否在此范围内。如果没有更多关于您的用例的详细信息,我不认为继续这个线程有意义。已经有一个问题询问如何在Rust中模拟一个库。 - undefined
1个回答

0
通过"chronos库"和"使用UTC::now",我将假设chrono::Utc::now是指的明确方法。声称"这几乎不可能编写一个测试"是不正确的,因为条件编译(相关线程)可以用于替换给定项的使用声明,以便在测试运行时更好地控制模拟或伪造版本。
举个快速的例子,这是一个函数,它会返回提供的DateTime是否在未来:
use chrono::{DateTime, Utc};

pub fn in_the_future(dt: DateTime<chrono::Utc>) -> bool {
    dt > Utc::now()
}

为了允许注入模拟版本的Utcuse声明应该被修改以确保测试使用模拟/存根版本,正常构建使用真实版本,条件编译应该用于Utc,因此用以下内容替换原始内容:
#[cfg(test)]
use crate::mock_chrono::Utc;
#[cfg(not(test))]
use chrono::Utc;
// the other use will remain unchanged.
use chrono::DateTime;

上述配置了模块在非测试构建时使用实际的chrono::Utc,而在测试构建中使用模拟版本。
现在来实现模拟本身 - 首先,提供与原方法相同签名的now()方法,返回可以从测试中控制的某个值。模块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));
}

这个模块导出了两个符号 - Utc 提供了一个自定义的 now() 方法,可以返回一个完全可控的 chrono::DateTime<chrono::Utc> 对象,以及函数 set_timestamp,它接受一个从 Unix 纪元开始的 i64 偏移量(为了数据结构的简单性而这样做,设计需要时可以更复杂)。在测试中使用可能会像这样:
#[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"),
    ));
}

完整的 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()
    }
}

有人可能会问为什么需要使用 thread_local!(),难道不能用更简单的 static mut 来进行这个简单的测试吗?嗯,虽然可以创建一个全局可变的单例并使用它,但那是非常错误的方法,因为测试通常在自己的线程中并发运行。与其让每个线程试图争夺对一个变量的控制(这要么导致每个测试相互覆盖,要么创建瓶颈),不如为每个线程提供其自己的线程本地变量,这才是合乎逻辑和明智的选择。

事实证明,一个解决类似需求的库 mock_instant 也以类似的方式使用了线程本地变量{{link5:in a similar manner}}。


很不幸,以这种方式进行模拟存在一个非常严重的限制:#[cfg(test)] 仅适用于当前被测试的 crate,而不适用于其任何依赖项。这意味着如果您有任何依赖项,现在需要为每个依赖项引入 _features_,并在 crate 被测试时启用该 feature(https://dev59.com/ZV4c5IYBdhLWcg3weqW9)。这会产生大量样板代码,并且现在有可能在测试之外意外启用该 feature... - undefined
好的,我觉得我理解了你对副作用、内部调用和依赖性的意思。为了更好地演示例子,我去掉了所有其他无关的分散注意力的琐事(这些琐事是我从我的一个原型项目中借鉴来的),只剩下一个非常简单的函数。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接