1 个不稳定版本
使用旧的 Rust 2015
0.3.1 | 2016 年 9 月 13 日 |
---|
#1209 在 算法
63KB
1.5K SLoC
QuickCheck 是一种使用随机生成的输入进行属性测试的方法。该软件包包含随机生成和缩小整数、浮点数、元组、布尔值、列表、字符串、选项和结果的能力。QuickCheck 所需的只是一个属性函数——然后它将随机生成该函数的输入并针对每组输入调用属性。如果属性失败(无论是由于运行时错误,如索引越界,还是不满足您的属性),则输入将被“缩小”以找到更小的反例。
列表和数字的缩小策略使用二分搜索快速覆盖输入空间。(它应该与 Koen Claessen 的 Haskell QuickCheck 中使用的相同策略。)
MIT 或 UNLICENSE 双许可。
文档
API 已完全文档化: http://burntsushi.net/rustdoc/quickcheck/.
简单示例
以下是一个测试反转向量的函数的示例
#[cfg(test)]
#[macro_use]
extern crate quickcheck;
fn reverse<T: Clone>(xs: &[T]) -> Vec<T> {
let mut rev = vec!();
for x in xs.iter() {
rev.insert(0, x.clone())
}
rev
}
#[cfg(test)]
mod tests {
quickcheck! {
fn prop(xs: Vec<u32>) -> bool {
xs == reverse(&reverse(&xs))
}
}
}
此示例使用 quickcheck!
宏,该宏在稳定 Rust 中可用。
#[quickcheck]
属性(需要 Rust 夜间版本)
为了更容易编写 QuickCheck 测试,#[quickcheck]
属性将属性函数转换为 #[test]
函数。
要使用 #[quickcheck]
属性,您必须启用 plugin
功能并将 quickcheck_macros
软件包作为语法扩展导入
#![feature(plugin)]
#![plugin(quickcheck_macros)]
#[cfg(test)]
extern crate quickcheck;
#[cfg(test)]
mod tests {
fn reverse<T: Clone>(xs: &[T]) -> Vec<T> {
let mut rev = vec!();
for x in xs {
rev.insert(0, x.clone())
}
rev
}
#[quickcheck]
fn double_reversal_is_identity(xs: Vec<isize>) -> bool {
xs == reverse(&reverse(&xs))
}
}
安装
quickcheck
在 crates.io
上,因此您可以将它包含到项目中,如下所示
[dependencies]
quickcheck = "0.3"
如果您的测试代码中只使用了 quickcheck
,则可以将它添加为开发依赖项
[dev-dependencies]
quickcheck = "0.3"
如果您想使用 #[quickcheck]
属性,请添加 quickcheck_macros
[dev-dependencies]
quickcheck = "0.3"
quickcheck_macros = "0.2"
并只为测试构建启用 quickcheck_macros
插件
#![cfg_attr(test, feature(plugin))]
#![cfg_attr(test, plugin(quickcheck_macros))]
注意,当 Rust 1.0 稳定版发布时,#[quickcheck]
宏将不会工作,尽管它将在夜间构建中继续工作。
注意:当使用 quickcheck
(无论是直接使用还是通过属性)时,RUST_LOG=quickcheck
启用 info!
以显示有用的输出(如通过测试的数量)。这不是显示失败证据所需的。
丢弃测试结果(或,属性是多态的!)
有时您只想测试对于可能输入的 子集 而言成立的属性,所以当您的属性被给定一个在这个子集之外的输入时,您会丢弃它。特别是,属性应该在您想要测试的子集之外的输入上既不通过也不失败。但是属性返回布尔值——这表示通过或失败。
为了解决这个问题,我们需要退后一步,看看 quickcheck
函数的类型
pub fn quickcheck<A: Testable>(f: A) {
// elided
}
所以 quickcheck
可以测试任何满足 Testable
特质的类型。很好,那么这个 Testable
是什么意思?
pub trait Testable {
fn result<G: Gen>(&self, &mut G) -> TestResult;
}
此特质表明,如果类型可以给定一个随机数源产生一个 TestResult
,则类型是可测试的。(TestResult
存储有关测试结果的信息,例如它是否通过、失败或被丢弃。)
确实如此,bool
满足 Testable
特质
impl Testable for bool {
fn result<G: Gen>(&self, _: &mut G) -> TestResult {
TestResult::from_bool(*self)
}
}
但在示例中,我们向 quickcheck
提供了一个 函数。是的,函数也可以满足 Testable
!
impl<A: Arbitrary + Debug, B: Testable> Testable for fn(A) -> B {
fn result<G: Gen>(&self, g: &mut G) -> TestResult {
// elided
}
}
这意味着一个函数满足 Testable
,当且仅当它有一个单一参数类型(其值可以随机生成和缩小)并返回任何类型(也满足 Testable
)。因此,类型为 fn(usize) -> bool
的函数满足 Testable
,因为 usize
满足 Arbitrary
且 bool
满足 Testable
。
所以,为了丢弃一个测试,我们需要返回除了 bool
之外的东西。如果我们直接返回一个 TestResult
会怎样?那应该可以工作,但我们需要确保 TestResult
满足 Testable
impl Testable for TestResult {
fn result<G: Gen>(&self, _: &mut G) -> TestResult { self.clone() }
}
现在我们可以直接测试返回 TestResult
的函数。
作为一个例子,让我们测试我们的 reverse 函数,以确保长度为 1 的向量的反转等于自身。
fn prop(xs: Vec<isize>) -> TestResult {
if xs.len() != 1 {
return TestResult::discard()
}
TestResult::from_bool(xs == reverse(&xs))
}
quickcheck(prop as fn(Vec<isize>) -> TestResult);
(此示例的完整工作程序在 examples/reverse_single.rs
。)
现在我们的属性返回一个 TestResult
,这使我们能够编码更多的信息。为 TestResult
类型定义了一些额外的便捷函数。[更多详情](http://burntsushi.net/rustdoc/quickcheck/struct.TestResult.html) 。例如,我们不能只是返回一个 bool
,所以我们将一个 bool
值转换为 TestResult
。
(忽略测试的能力可以让你获得类似 Haskell 的 ==>
组合子。)
注意:由于忽略测试意味着它既不通过也不失败,因此 quickcheck
将尝试用一个全新的测试来替换被忽略的测试。然而,如果你的条件很少满足,则 quickcheck
可能不得不运行比平常更少的测试。默认情况下,如果 quickcheck
在尝试了 10,000
次后,仍然找不到 100
个有效的测试,那么它将放弃。此参数可以使用 quickcheck_config
进行更改。
缩小
缩小是 QuickCheck 的关键部分,它自动简化了属性的逆例。例如,如果你错误地将一个用于反转向量的函数定义为:(对于这个精心制作的例子,我表示歉意)
fn reverse<T: Clone>(xs: &[T]) -> Vec<T> {
let mut rev = vec![];
for i in 1..xs.len() {
rev.insert(0, xs[i].clone())
}
rev
}
然后有一个用于测试的属性,检查 xs == reverse(reverse(xs))
fn prop(xs: Vec<isize>) -> bool {
xs == reverse(&reverse(&xs))
}
quickcheck(prop as fn(Vec<isize>) -> bool);
那么如果没有缩小,你可能会得到这样的逆例
[quickcheck] TEST FAILED. Arguments: ([-17, 13, -12, 17, -8, -10, 15, -19,
-19, -9, 11, -5, 1, 19, -16, 6])
这相当神秘。但启用缩小后,你几乎可以保证每次都得到这个逆例
[quickcheck] TEST FAILED. Arguments: ([0])
这将更容易调试。
案例分析:埃拉托斯特尼筛法
埃拉托斯特尼筛法 是一种简单而优雅的方法,可以找到所有小于或等于 N
的素数。简要地说,算法通过分配一个包含 N
个槽的数组,其中包含布尔值来工作。标记为 false
的槽对应于素数(或构建筛子时不知道是否为素数的数字),而标记为 true
的槽则已知不是素数。对于每个 n
,此数组中所有 n
的倍数都被标记为 true。当所有 n
都已检查后,标记为 false
的数字被返回作为素数。
正如你所想象的那样,这里有很多潜在的越界错误,这使得它非常适合随机测试。所以让我们来看看我的实现,看看我们是否能找到错误。
fn sieve(n: usize) -> Vec<usize> {
if n <= 1 {
return vec![];
}
let mut marked = vec![false; n+1];
marked[0] = true;
marked[1] = true;
marked[2] = true;
for p in 2..n {
for i in (2*p..n).filter(|&n| n % p == 0) {
marked[i] = true;
}
}
marked.iter()
.enumerate()
.filter_map(|(i, &m)| if m { None } else { Some(i) })
.collect()
}
让我们手动尝试几个输入
sieve(3) => [2, 3]
sieve(5) => [2, 3, 5]
sieve(8) => [2, 3, 5, 7, 8] # !!!
出了点问题!但问题出在哪里?这个错误相当微妙,但很容易犯。如果你找不到它,那没关系,因为我们将使用 QuickCheck 来帮助我们追踪它。
在查看一些示例输出之前,最好尝试想出一些总是可以通过函数输出满足的 属性。对于素数筛子来说,一个明显的属性是检查返回的所有数字是否都是素数。为此,我们需要一个 is_prime
函数
fn is_prime(n: usize) -> bool {
n != 0 && n != 1 && (2..).take_while(|i| i*i <= n).all(|i| n % i != 0)
}
这段代码所做的事情是检查 [2, sqrt(n)]
区间内的任何数字是否能整除 n
,并且对于 0
和 1
这两个基本情况也进行了处理。
现在我们可以编写我们的 QuickCheck 属性了。
fn prop_all_prime(n: usize) -> bool {
sieve(n).into_iter().all(is_prime)
}
最后,我们需要使用我们的属性来调用 quickcheck
。
fn main() {
quickcheck(prop_all_prime as fn(usize) -> bool);
}
包含这段代码的完整源文件可以在 examples/sieve.rs
中找到。
运行此程序后的输出有这条消息:
[quickcheck] TEST FAILED. Arguments: (4)
它说明当给定 n = 4
时,sieve
失败了 prop_all_prime
测试。由于缩小(shrinking),它能够找到一个(希望是)最小的反例。
对于如此简短的反例,找到错误所在可能更容易一些。由于返回 4
,它可能从未被标记为非质数。由于 4
是 2
的倍数,所以当 p = 2
时,这些行上它的槽应该被标记为 true
。
for i in (2*p..n).filter(|&n| n % p == 0) {
marked[i] = true;
}
啊!但 ..
(范围)操作符是否包含 n
?不!这个特定的操作符是一个半开区间。
2*p..n
范围在 n = 4
时永远不会产生 4
。当我们将其更改为 2*p..n+1
时,所有测试都通过。
此外,如果我们的错误导致索引越界错误,那么 quickcheck
可以像处理任何其他失败一样处理它——包括处理由运行时错误引起的失败。
但是等等……我们还没有完成。现在,我们的属性测试了由 sieve
返回的所有数字都是质数,但它没有测试列表是否完整。它不能确保找到 0
和 n
之间的所有质数。
这是一个更全面的属性。
fn prop_prime_iff_in_the_sieve(n: usize) -> bool {
sieve(n) == (0..(n + 1)).filter(|&i| is_prime(i)).collect::<Vec<_>>()
}
它测试对于 0 和 n 之间的每个数字(包括 n),朴素质数测试的结果与筛选器相同。
现在,如果我们运行它
fn main() {
quickcheck(prop_all_prime as fn(usize) -> bool);
quickcheck(prop_prime_iff_in_the_sieve as fn(usize) -> bool);
}
我们看到它立即因为 n = 2 的值而失败。
[quickcheck] TEST FAILED. Arguments: (2)
如果我们再次检查 sieve()
,我们会看到我们错误地将 2
标记为非质数。删除行 marked[2] = true;
使得两个属性都通过。
QuickCheck 的移植中缺少了什么?
我认为我已经捕捉到了关键特性,但仍然有一些东西缺失。
- 到目前为止,只有具有 4 个或更少参数的函数才能进行快速检查。这个限制可以提高到
N
,但这需要对每个n
实现一个Testable
特性的实现。 - 由于栈溢出而失败的函数不会被QuickCheck捕获。因此,此类失败将不会有证据附件。(我想解决这个问题,但不知道如何。)
Coarbitrary
在这个包的任何形式中都不存在。我认为这是可能的;我只是还没时间去做。
依赖关系
~4.5MB
~85K SLoC