Rust 中如何优雅地模拟当前时间并测试时间相关逻辑?
结论
在示例中,最简单的做法是将时间作为参数传入业务逻辑,只在调用入口读取系统时间。测试时传入固定的 Unix 时间戳,就能得到确定的结果,不需要专门创建 CurrentTime 库。
如果库中多个操作都要读取时间,或一次操作需要读取多次,可以考虑注入闭包或定义 Clock trait。
为什么不建议到处使用条件编译切换时间
原来的条件编译写法可以用于单元测试,但有两个限制:
#[cfg(test)]在编译时决定哪些代码参与构建,不能让同一次测试运行中的不同用例分别选择不同时间。- 集成测试通常放在
tests/目录。被测库作为依赖编译时,不会自动启用cfg(test),因此库内部仍会使用系统时间。
如果在条件编译分支中写入固定时间,测试就会使用专门的时间获取逻辑。将时间改为显式输入后,每个用例都能设置自己的边界值,业务逻辑依赖的条件也更清楚。
一次读取时间:把时间传给业务函数
示例可以拆成两部分:do_sth 读取系统时间并打印,message_at 根据时间生成输出。
use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
pub struct SomeLib;
impl SomeLib {
pub fn do_sth() -> Result<(), SystemTimeError> {
let unix_time = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_secs();
println!("{}", Self::message_at(unix_time));
Ok(())
}
fn message_at(unix_time: u64) -> String {
format!("Doing something at {} second", unix_time)
}
}
#[cfg(test)]
mod tests {
use super::SomeLib;
#[test]
fn formats_fixed_time() {
assert_eq!(
SomeLib::message_at(1770000000),
"Doing something at 1770000000 second"
);
}
#[test]
fn formats_epoch() {
assert_eq!(
SomeLib::message_at(0),
"Doing something at 0 second"
);
}
}
实际使用时,可以将原先依赖 unix_time 的计算放进相应的函数。测试只需检查返回值,不必为了验证业务输出而捕获标准输出。
示例还把原来的 unwrap() 改成了 ?,并将 do_sth 的返回类型改为 Result<(), SystemTimeError>。系统时间早于 UNIX_EPOCH 时,错误会交给调用方处理。这和时间注入是两项独立的改动,可以按库现有的错误处理约定调整。
这些测试验证的是根据时间生成输出的逻辑,不包括读取系统时间或实际打印。message_at 是私有函数,同一模块下的单元测试可以访问;外部集成测试需要通过公开接口测试。
需要测试完整调用流程时:注入时间提供者
如果测试需要经过读取时间的业务入口,可以让入口接收一个闭包。下面的独立简化示例将生成的消息作为返回值,方便断言:
fn do_sth_with_clock(now: impl FnOnce() -> u64) -> String {
let unix_time = now();
format!("Doing something at {} second", unix_time)
}
#[cfg(test)]
mod tests {
use super::do_sth_with_clock;
#[test]
fn uses_injected_time() {
let message = do_sth_with_clock(|| 1770000000);
assert_eq!(
message,
"Doing something at 1770000000 second"
);
}
}
正常调用时,由系统时间提供者读取时间,并按你的错误处理约定返回时间或错误。上面的闭包返回 u64,假定时间提供者已经处理了读取失败;如果需要向上传递错误,应将闭包返回类型改为合适的 Result。
示例只调用一次时间提供者,因此使用 FnOnce。如果需要反复读取,可以根据调用方式使用 Fn 或 FnMut。例如,模拟时间逐步前进的闭包可能需要修改内部状态,这时可以使用 FnMut。
什么时候值得定义 Clock trait
当多个组件共享时间来源,或时间提供者需要提供多个相关操作时,可以定义 Clock trait 并让库持有它。正常运行时使用系统时钟实现,测试时使用固定时间或可控时间实现。
只有一处读取时间时,直接传入 u64 或闭包通常就够了。是否需要单独拆成库,取决于是否存在跨组件、跨项目复用的需求。
还需要明确时间的含义:Unix 时间戳适用于日期和绝对时间判断;如果逻辑只关心经过了多久,通常应考虑 std::time::Instant,因为系统日历时间可能被调整。
备注:内容仅供参考。
