#property-testing #stateful #properties #proptest #quickcheck #test-cases

proptest-stateful

使用proptest crate构建状态性属性测试的库

4个版本

0.1.3 2023年7月3日
0.1.2 2023年5月25日
0.1.1 2023年5月24日
0.1.0 2023年5月23日

#403 in 测试

Apache-2.0

29KB
196 代码行

proptest-stateful

proptest-stateful 是由 ReadySet 创建的 Rust 库,用于使用 proptest crate 编写状态性基于属性的测试。

API参考文档.

简介

基于属性的测试通常是无状态的:它们生成一个输入,调用一个函数,并检查输出的一些属性。它们这样做而不考虑正在测试的系统中的任何底层状态。

proptest-stateful 将这种测试范式扩展到有状态系统:状态性属性测试生成要运行的操作序列,一次执行一个,并检查每个操作的过程后条件。如果测试失败,proptest-stateful 将尝试删除测试序列中的单个步骤,以找到最小化失败案例。

proptest-stateful 提供了一个用于定义状态性属性测试的trait,包括用于指定生成策略、模型状态、先决条件和后置条件的回调。一旦定义了这些,proptest-stateful 代码就可以进行生成有效测试用例、运行测试和缩小测试失败的重任。

快速入门

假设您想测试您创建的一个简单的 Counter 结构体

struct Counter {
    count: usize,
}

impl Counter {
    fn new(count: usize) -> Self {
        Counter { count }
    }

    fn inc(&mut self) {
        self.count += 1;
    }

    fn dec(&mut self) {
        self.count -= 1;
    }
}

在测试用例的开始,我们将创建一个新的 Counter,并生成一系列 incdec 操作,因此我们首先需要定义一个 Operation 类型来表示这些操作

#[derive(Clone, Debug)]
enum CounterOp {
    Inc,
    Dec,
}

API要求我们定义一个状态类型,尽管目前它将是空的

#[derive(Clone, Debug, Default)]
struct TestState {}

我们还需要一个上下文字型来在执行测试用例时持有运行时状态

struct TestContext {
    counter: Counter,
}

现在我们只需要定义回调来告诉框架如何生成和运行测试用例。(我们目前只支持异步测试用例,因此这比实际需要的复杂一些,因为在这个测试中实际上并不需要异步,但它仍然可以正常工作。)

#[async_trait(?Send)]
impl ModelState for TestState {
    type Operation = CounterOp;
    type RunContext = TestContext;
    type OperationStrategy = BoxedStrategy<Self::Operation>;

    fn op_generators(&self) -> Vec<Self::OperationStrategy> {
        // For each step test, arbitrarily pick Inc or Dec, regardless of the test state:
        vec![Just(CounterOp::Inc).boxed(), Just(CounterOp::Dec).boxed()]
    }

    // No preconditions to worry about or test state to maintain yet
    fn preconditions_met(&self, _op: &Self::Operation) -> bool {
        true
    }
    fn next_state(&mut self, _op: &Self::Operation) {}

    async fn init_test_run(&self) -> Self::RunContext {
        let counter = Counter::new(3); // Start with 3 to make the failing cases more interesting
        TestContext { counter }
    }

    async fn run_op(&self, op: &Self::Operation, ctxt: &mut Self::RunContext) {
        match op {
            CounterOp::Inc => ctxt.counter.inc(),
            CounterOp::Dec => ctxt.counter.dec(),
        }
    }

    async fn check_postconditions(&self, _ctxt: &mut Self::RunContext) {}
    async fn clean_up_test_run(&self, _ctxt: &mut self::runcontext) {}
}

最后,您可以通过以下方式运行测试

#[test]
fn run_cases() {
    let config = ProptestStatefulConfig {
        min_ops: 10,
        max_ops: 20,
        test_case_timeout: Duration::from_secs(60),
        proptest_config: ProptestConfig::default(),
    };

    proptest_stateful::test::<TestState>(config);
}

如果您运行此代码,应该会很快看到失败,因为我们没有考虑到下溢!如果一个测试用例导致计数器的值降至0以下,测试将会失败。然后测试将继续缩小失败的用例,这将使您从看似随机的增减操作字符串变为以下情况

minimal failing input: [
    Dec,
    Dec,
    Dec,
    Dec,
]

(由于我们从计数器3开始,我们需要减4次才能触发下溢。)

修复此示例

现在假设下溢是一个已知的限制,所以我们不希望测试会触发下溢恐慌的用例。为了做到这一点,我们需要维护一个实际的状态模型,以了解我们期望的当前计数器状态

struct TestState {
    model_count: usize,
}

impl Default for TestState {
    fn default() -> Self {
        TestState { model_count: 3 } // Set to match initial test value
    }
}

为了在生成测试步骤时保持其更新,我们实现了next_state

    fn next_state(&mut self, op: &Self::Operation) {
        match op {
            CounterOp::Inc => {
                self.model_count += 1;
            }
            CounterOp::Dec => {
                self.model_count -= 1;
            }
        }
    }

现在我们可以将其用于生成器和先决条件

    fn op_generators(&self) -> Vec<Self::OperationStrategy> {
        let mut ops = vec![Just(CounterOp::Inc).boxed()];
        if self.model_count > 0 {
            ops.push(Just(CounterOp::Dec).boxed());
        }
        ops
    }

    fn preconditions_met(&self, op: &Self::Operation) -> bool {
        match op {
            CounterOp::Inc => true,
            CounterOp::Dec => self.model_count > 0,
        }
    }

运行此测试现在应该通过,因为它将不会生成使计数器值降至0以下的测试用例。

您可以通过tests/counter.rs文件查看完成的代码并自行运行。

对于更复杂的实际示例,请查看我们为ReadySet编写的这些测试套件

贡献

我们欢迎贡献!请查看我们的问题页面,如果您想处理任何悬而未决的票据,或者如果您有任何其他想法的修复或改进,欢迎与我们联系。

依赖项

~5–12MB
~115K SLoC