PAYATHON 2026

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,因为系统日历时间可能被调整。

备注:内容仅供参考。