7 个版本
0.2.1 | 2023年5月3日 |
---|---|
0.2.0 | 2023年5月3日 |
0.1.4 | 2023年5月3日 |
0.1.3 | 2023年4月30日 |
#244 in 测试
每月下载量 70
25KB
367 代码行
Sleuth
极其有见地的测试框架,生成精确规范并简化代码到最小实现。
概述
此库将变异测试的理念推向极致。
变异测试认为你源代码中的小错误(如 a > b
而不是 a >= b
)很可能是错误;当它们发生时,你的测试应该失败,变异测试框架确保它们这样做。当你的测试套件无法识别一个错误时,这意味着你需要添加一个新的测试。
Rust 已经有我非常钦佩且不打算取代的变异测试框架。此库是对变异 所有 代码的实验,通过蛮力证明没有比你所写的函数更短的函数能满足你的规范。它应该鼓励测试驱动开发,无需在事先编写详尽的测试套件;经过初步尝试后,你的测试和实现将随着你遇到所有可能的实现方式而共同进化,最终缩小到你最初想要的目标。
此库还受到了像 Haskell 的 QuickCheck
这样的属性测试的启发。你可以使用可重用的属性编写测试,并通过 !
指示它不要通过,如下所示
use ::sleuth::sleuth;
fn roundtrip<T, U, F, G>(f: F, g: G, x: T) -> bool
where
T: PartialEq + Clone,
F: Fn(U) -> T,
G: Fn(T) -> U,
{
x.clone() == f(g(x))
}
#[sleuth(
roundtrip(sub_one, 42),
!roundtrip(add_one, 42),
)]
fn add_one(x: u8) -> u8 {
x + 1
}
#[sleuth(
roundtrip(add_one, 42),
!roundtrip(sub_one, 42),
)]
fn sub_one(x: u8) -> u8 {
x - 1
}
注意以下几点
roundtrip
返回一个布尔值来指示成功或失败,而不是像常规测试一样panic
。sleuth
属性接受一个带有缺少第一个参数的函数(这里,roundtrip
没有提供f
),该参数在编译时用应用该属性到的函数(这里,add_one
和sub_one
)填充。- 我们不向
roundtrip
发送大量随机输入(尽管你可以单独这样做!)。相反,你选择你的输入(在这里,两次都是42
),库帮助你找到能够排除所有变异的最小集合。
库仍在开发中——我是一名大学生,经常缺乏空闲时间——但当前,它可以运行像上面的测试套件,并且不久之后应该能够简化以下常见的案例
fn is_true(b: bool) -> bool {
if b { true } else { false }
}
到它们的最小实现
fn is_true(b: bool) -> bool {
b
}
这通常是变异测试无法(也不旨在)修复的。Clippy可以基于白名单修复这些简单的错误,但这个库旨在从底层消除整个类别的错误,而不是玩击鼓传花。
它是如何工作的
给定一个像这样的函数
#[sleuth(
does_nothing(false),
does_nothing(true),
)]
const fn is_true(b: bool) -> bool {
if b { true } else { false }
}
sleuth
属性在编译时将其重写为以下内容
const fn is_true(b: bool) -> bool {
if b { true } else { false }
}
#[cfg(test)]
mod is_true_sleuth {
use super::*;
// A struct with each scoped variable.
struct Scope { pub b: bool }
// A fully implemented AST node for each function argument.
mod scoped_variables {
pub struct B; // CamelCase by convention but referring to the argument `b`
impl ::sleuth::Expr for B {
type Return = bool;
type Scope = super::Scope;
const COMPLEXITY: usize = 1;
fn eval(&self, scope: &mut Self::Scope) -> Self::Return {
scope.b
}
}
}
// Instantiation of a unique type for the exact AST of `is_true`.
type Ast = ::sleuth::expr::{... many lines, lots of generics ...};
const AST: Ast = ::sleuth::expr::{... instantiation of the above ...};
// Checks, for any function, whether it fulfills the test suite for `is_true`.
pub fn check<_FnToCheck>(f: &_FnToCheck) -> Option<&'static str>
where
_FnToCheck: Fn(bool) -> bool + ::core::panic::RefUnwindSafe
{
match std::panic::catch_unwind(|| crate::sleuth::does_nothing(f, false)) {
Ok(b) => if !b { Some("crate::sleuth::does_nothing(f, false)") } else { None },
_ => Some("crate::sleuth::does_nothing(f, false) PANICKED (see two lines above)"),
}
.or_else(|| match std::panic::catch_unwind(|| crate::sleuth::does_nothing(f, true)) {
Ok(b) => if !b { Some("crate::sleuth::does_nothing(f, true)") } else { None },
_ => Some("crate::sleuth::does_nothing(f, true) PANICKED (see two lines above)"),
})
}
#[test]
fn test_original() {
// check that the original function passes before mutating
::sleuth::testify(check(&is_true))
}
#[test]
fn test_mutants() {
use ::sleuth::Expr;
// If there's something wrong with our AST, pass and don't mutate
// And if it's an error in your logic, `test_original` won't pass
if check(&(|b| AST.eval(&mut Scope { b }))).is_some() { return; }
// breadth-first search
for mutation_severity in 0usize..=AST::COMPLEXITY {
// . . .
// very long, not yet complete
// . . .
// eventually, with a different value in place of `AST` each time:
::sleuth::testify(check(&(|b| AST.eval(&mut Scope { b }))))
}
}
}
你可以通过运行EXPAND=1 cargo expand
来查看这个展开。环境变量EXPAND
将#[cfg(test)]
和#[test]
转换为无意义的内容,这样cargo expand
就不会假设我们不是在测试,并删除整个模块。
注意,除非我们在测试中,否则该宏没有任何效果。即使在生产中也可以安全使用。
依赖关系
~0.6–11MB
~77K SLoC