功能说明
依赖系统时间的测试通常难以稳定:等待令牌过期需要真实睡眠,测试变慢;时间边界附近的断言又可能因调度抖动而偶发失败。假时钟把当前时间抽象为可读写值,业务代码只依赖注入的时钟接口,测试中手动推进时间,生产环境则使用真实时钟。这样既不需要睡眠,也能精确验证到期前一毫秒和到期瞬间的行为。
本例用 Arc 包裹原子整数保存毫秒数,克隆时钟时共享同一时间源。Token 记录绝对过期时刻,issue 用当前时间加有效期,expired 比较当前时间是否已达到过期点。演示推进五十九秒时未过期,再推进一秒后过期。
关键代码
FakeClock 的毫秒值是 AtomicU64,多个克隆可跨线程读取和推进,顺序一致性保证测试中的可见性。now 把毫秒转换为 Duration,advance 使用 fetch_add 原子累加。Token 只保存 expires_at,不保存创建时间,避免逻辑依赖时间差。issue 和 expired 都接收时钟引用,而不是内部读取系统时间,这是可测试性的关键。边界比较使用大于等于,使到达过期时刻即视为过期。
使用注意
真实系统应使用 Instant 计算时长、用 SystemTime 表示绝对时间,本例为了简化全部使用 Duration,不能直接映射到真实日历时间。假时钟的推进是显式的,后台任务若依赖时钟自动前进,需要测试手动协调。跨线程测试要注意推进与读取的顺序,必要时用屏障同步。生产代码中不要允许外部修改时钟,否则会引入安全风险。该模式适合单元测试,集成测试仍应覆盖少量真实时间行为。
验证方式
运行脚本,第一行输出 expired at 59s: false,第二行输出 expired at 60s: true,准确覆盖到期边界。把有效期改为零,创建后立即应为过期。重复运行测试多次,结果应完全一致,且总耗时不随有效期增长。
代码示例
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
struct FakeClock {
millis: Arc<AtomicU64>,
}
impl FakeClock {
fn new() -> Self {
Self {
millis: Arc::new(AtomicU64::new(0)),
}
}
fn now(&self) -> Duration {
Duration::from_millis(self.millis.load(Ordering::SeqCst))
}
fn advance(&self, delta: Duration) {
self.millis
.fetch_add(delta.as_millis() as u64, Ordering::SeqCst);
}
}
struct Token {
expires_at: Duration,
}
impl Token {
fn issue(clock: &FakeClock, ttl: Duration) -> Self {
Self {
expires_at: clock.now() + ttl,
}
}
fn expired(&self, clock: &FakeClock) -> bool {
clock.now() >= self.expires_at
}
}
fn main() {
let clock = FakeClock::new();
let token = Token::issue(&clock, Duration::from_secs(60));
clock.advance(Duration::from_secs(59));
println!("expired at 59s: {}", token.expired(&clock));
clock.advance(Duration::from_secs(1));
println!("expired at 60s: {}", token.expired(&clock));
}