2 个不稳定版本

0.1.0 2021年2月14日
0.0.0 2020年10月13日

#357测试

每月38 次下载

MIT/Apache

42KB
844

chronobreak: Rust 确定性时间测试模拟库

crates.io docs.rs build status coverage

chronobreak 是一个测试模拟库,用于确定地测试给定测试主题的任何基于时间的属性。

动机

假设我们编写了一个简单的函数,该函数在达到给定的时间点后返回某个值

use std::time::*;
use std::thread;

fn return_at<T>(time: Instant, t: T) -> T {
    if Instant::now() < time {
        thread::sleep(time.saturating_duration_since(Instant::now()));
    }
    t
}

现在我们可能想测试这个函数是否真的像预期的那样休眠

#[test]
fn test_return_at() {
    let return_time = Instant::now() + Duration::from_secs(1);
    return_at(return_time, 0);
    assert_eq! {Instant::now(), return_time};
}

这个测试用例很可能会失败。解决这个问题的常见策略是预期时间在某个区间内,而不是比较精确的相等。但这种方法永远不能保证类似的测试用例能够确定性地成功。

chronobreak 来帮忙

那么我们如何确定性地通过测试呢?

首先,为了使模拟时钟按预期工作,对于 chronobreak 提供模拟的每个导入,使用模拟来编译测试非常重要

#[chronobreak]
use std::time::*; // will be replaced with `use chronobreak::mock::std::time::*; for tests
#[chronobreak]
use std::thread;

为了让不故意遗漏任何模拟变得尽可能容易,chronobreak 还重新导出了不需要模拟的受支持库中的所有项目。

现在我们可以通过简单地用 #[test] 替换 #[chronobreak::test] 来使用模拟时钟进行测试

#[chronobreak::test]
fn test_return_at() {
    let return_time = Instant::now() + Duration::from_secs(1);
    return_at(return_time, 0);
    assert_eq! {Instant::now(), return_time};
}

这里发生的事情是,模拟版本的 thread::sleep 将作为原始函数的装饰器。如果当前测试用例的时钟被模拟,它将向前推进整整一秒。如果没有被模拟,thread::sleep 将直接委托给原始函数。

冻结时钟

除了自动前进时钟的默认行为之外,Chronobreak 允许冻结时钟。这导致所有定时等待都将阻塞,直到其他线程通过手动调用 clock::advanceclock::advance_to 或在未冻结状态下执行定时等待来前进时钟。

此功能主要旨在与 extended-apis 功能结合使用,该功能将 Thread::expect_timed_waitJoinHandle::expect_timed_wait 添加到这些类的模拟版本的公共 API 中。这些函数使得在恢复之前等待另一个线程进入定时等待成为可能。这对于测试主题是并发数据结构的情况很有用,必须测试当它从一条线程接收输入时,同时它已经在另一条线程上进入了定时等待时,其行为是否正确。

许可协议

在以下许可协议下使用:

任选其一。

贡献

除非你明确说明,否则根据 Apache-2.0 许可证定义的,你提交的任何有意包含在 Chronobreak 中的贡献,将按照上述方式双重许可,不附加任何额外的条款或条件。

依赖项

约 8-17MB
约 243K SLoC