我有一个方法,在其实现中使用了UTC::now,这肯定会使测试变得几乎不可能。
时间功能正在使用chronos库。
现在我基本上正在寻找可用的机制来模拟/存根Rust测试中的时间。
有人对如何处理这种情况有任何建议吗?
我有一个方法,在其实现中使用了UTC::now,这肯定会使测试变得几乎不可能。
时间功能正在使用chronos库。
现在我基本上正在寻找可用的机制来模拟/存根Rust测试中的时间。
有人对如何处理这种情况有任何建议吗?
chrono::Utc::now
是指的明确方法。声称"这几乎不可能编写一个测试"是不正确的,因为条件编译(相关线程)可以用于替换给定项的使用声明,以便在测试运行时更好地控制模拟或伪造版本。DateTime
是否在未来:use chrono::{DateTime, Utc};
pub fn in_the_future(dt: DateTime<chrono::Utc>) -> bool {
dt > Utc::now()
}
Utc
,use
声明应该被修改以确保测试使用模拟/存根版本,正常构建使用真实版本,条件编译应该用于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
UTC::now
然后调用您的函数,然后再次执行UTC::now
,并检查返回的结果是否在此范围内。如果没有更多关于您的用例的详细信息,我不认为继续这个线程有意义。已经有一个问题询问如何在Rust中模拟一个库。 - undefined