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 测试
29KB
196 代码行
proptest-stateful
proptest-stateful
是由 ReadySet 创建的 Rust 库,用于使用 proptest crate 编写状态性基于属性的测试。
简介
基于属性的测试通常是无状态的:它们生成一个输入,调用一个函数,并检查输出的一些属性。它们这样做而不考虑正在测试的系统中的任何底层状态。
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
,并生成一系列 inc
和 dec
操作,因此我们首先需要定义一个 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