← 中文资源

简体中文版本

用于确定性测试的假时钟

向令牌过期逻辑注入可手动推进的时钟,让测试无需真实等待,并能精确覆盖到期边界。

阅读英文原文

功能说明

依赖系统时间的测试通常难以稳定:等待令牌过期需要真实睡眠,测试变慢;时间边界附近的断言又可能因调度抖动而偶发失败。假时钟把当前时间抽象为可读写值,业务代码只依赖注入的时钟接口,测试中手动推进时间,生产环境则使用真实时钟。这样既不需要睡眠,也能精确验证到期前一毫秒和到期瞬间的行为。

本例用 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));
}