当前位置: 首页 > news >正文

Rust之自动化测试(一):如何编写测试

开发环境

  • Windows 10
  • Rust 1.71.1

 

  • VS Code 1.81.1

 项目工程

这里继续沿用上次工程rust-demo

编写自动化测试

Edsger W. Dijkstra在他1972年的文章《谦逊的程序员》中说,“程序测试可以是一种非常有效的方法来显示错误的存在,但它对于显示它们的不存在是完全不够的。”这并不意味着我们不应该尽可能多地尝试测试! 

我们程序的正确性是指我们的代码在多大程度上做了我们想要它做的事情。Rust的设计高度关注程序的正确性,但正确性很复杂,不容易证明。Rust的类型系统承担了这个负担的很大一部分,但是类型系统不能捕获所有的东西。因此,Rust包含了对编写自动化软件测试的支持。

假设我们编写了一个函数add_two,它将传递给它的任何数字加2。这个函数的签名接受一个整数作为参数,并返回一个整数作为结果。当我们实现和编译这个函数时,Rust会进行所有的类型检查和借用检查,就像你到目前为止所学的那样,以确保我们不会向这个函数传递一个String值或一个无效的引用。但是Rust不能检查这个函数是否能准确地达到我们的目的,即返回参数加2,而不是参数加10或参数减50!这就是测试的由来。

我们可以编写一些测试来断言,例如,当我们将3传递给add_two函数时,返回值是5。每当我们对代码进行更改时,我们都可以运行这些测试,以确保任何现有的正确行为都没有改变。

测试是一项复杂的技能:虽然我们无法在一章中涵盖如何编写好的测试的每个细节,但我们将讨论Rust测试工具的机制。我们将讨论编写测试时可用的注释和宏,为运行测试提供的默认行为和选项,以及如何将测试组织成单元测试和集成测试。

如何编写测试

测试是Rust函数,它验证非测试代码是否以预期的方式运行。测试函数的主体通常执行这三个动作:

  1. 设置任何需要的数据或状态。
  2. 运行您想要测试的代码。
  3. 断言结果是你所期望的。

让我们来看看Rust专门为编写执行这些操作的测试提供的特性,包括test属性、一些宏和should_panic属性。

测试函数的剖析

最简单地说,Rust中的测试是一个用test属性注释的函数。属性是关于Rust代码片段的元数据;一个例子是我们在第5章中对结构使用的derive属性。要将函数更改为测试函数,请在fn之前的行中添加#[test]。当您使用cargo test命令运行您的测试时,Rust会构建一个测试运行二进制文件,运行带注释的函数并报告每个测试函数是通过还是失败。

每当我们用Cargo创建一个新的库项目时,就会自动为我们生成一个包含测试功能的测试模块。这个模块为您提供了一个编写测试的模板,这样您就不必在每次开始一个新项目时都去查找精确的结构和语法。您可以添加任意多的额外测试函数和测试模块!  

在实际测试任何代码之前,我们将通过模板测试来探索测试工作的某些方面。然后,我们将编写一些真实世界的测试,调用我们编写的一些代码,并断言其行为是正确的。 

让我们创建一个名为adder的新库项目,它将添加两个数:

$ cargo new adder --libCreated library `adder` project
$ cd adder

 adder库中src/lib.rs文件的内容应该如示例11-1所示。

文件名:src/lib.rs

#[cfg(test)]
mod tests {         #[test]                       // 测试fn it_works() {let result = 2 + 2;assert_eq!(result, 4);}
}

示例11-1:由cargo new自动生成的测试模块和函数

现在,让我们忽略上面两行,把注意力集中在函数上。注意#[test]注释:这个属性表明这是一个测试函数,所以测试运行人员知道将这个函数视为一个测试。在tests模块中,我们还可能有非测试函数来帮助设置常见的场景或执行常见的操作,所以我们总是需要指出哪些函数是测试。

示例函数体使用了assert_eq!宏来断言包含2和2相加result的结果等于4。这个断言作为一个典型测试格式的例子。让我们运行它来看看这个测试是否通过。 

cargo test命令运行我们项目中的所有测试,如示例11-2所示。

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.57sRunning unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::it_works ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

示例11-2:运行自动生成的测试的输出 

 Cargo编译并运行了测试。我们看到runing1 test的行。下一行显示了生成的测试函数的名称,名为it_works,运行该测试的结果是ok的。总体总结test result: ok。意味着所有的测试都通过了,而写着1 passed; 0 failed总计测试通过或失败的次数。 

0 measured测量统计值用于测量性能的基准测试。在撰写本文时,基准测试只在夜间Rust中可用。

Doc-tests adder开始的测试输出的下一部分是任何文档测试的结果。我们还没有任何文档测试,但Rust可以编译任何出现在我们API文档中的代码示例。这个特性有助于保持您的文档和代码同步!现在,我们将忽略Doc-tests输出。

让我们开始根据自己的需要定制测试。首先将it_works函数的名称改为不同的名称,例如exploration,如下所示:

文件名:src/lib.rs

#[cfg(test)]
mod tests {#[test]fn exploration() {assert_eq!(2 + 2, 4);}
}

然后再次运行cargo test。现在输出显示的是exploration而不是it_works:

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.59sRunning unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::exploration ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

 现在我们将添加另一个测试,但这次我们将做一个失败的测试!当测试函数出现问题时,测试就会失败。每个测试都在一个新线程中运行,当主线程发现一个测试线程已经死亡时,该测试就会被标记为失败。在第9章中,我们谈到了恐慌的最简单的方法是如何打电话给panic!宏观。输入新的测试作为一个名为another的函数,这样你的src/lib.rs文件看起来如示例11-3所示。

文件名:src/lib.rs

#[cfg(test)]
mod tests {#[test]fn exploration() {assert_eq!(2 + 2, 4);}#[test]fn another() {panic!("Make this test fail");}
}

 示例11-3:添加第二个测试,该测试将失败,因为我们称之为panic!宏指令

使用cargo test再次运行测试。输出应该如示例11-4所示,这表明我们的exploration测试通过了,而another测试失败了。 

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.72sRunning unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)running 2 tests
test tests::another ... FAILED
test tests::exploration ... okfailures:---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::anothertest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass `--lib`

示例11-4:当一个测试通过而另一个测试失败时的测试结果

 行test tests::another显示FAILED,而不是ok。在单独的结果和总结之间出现两个新的部分:第一部分显示每个测试失败的详细原因。在这种情况下,我们获得another失败的详细信息,因为它在src/lib.rs文件的第10行panicked at 'Make this test fail'时死机。 

总结行显示在最后:总的来说,我们的测试结果是FAILED的。我们有一个测试通过,一个测试失败。 

现在您已经看到了不同场景下的测试结果,让我们来看看除了panic!之外的一些宏在测试中很有用。 

用assert!宏检查结果

assert!当您希望确保测试中的某个条件评估为真时,标准库提供的宏非常有用。我们给出assert!计算结果为布尔值的参数。如果该值为true,则什么都不会发生,测试通过。如果值为false,则assert!调用panic!导致测试失败。使用assert!帮助我们检查我们的代码是否按照我们想要的方式运行。

在第五章的示例5-15中,我们使用了一个Rectangular结构和一个can_hold方法,它们在示例11-5中重复出现。让我们将这段代码放到src/lib.rs文件中,然后使用assert!为它编写一些测试。

 文件名:src/lib.rs

#[derive(Debug)]
struct Rectangle {width: u32,height: u32,
}impl Rectangle {fn can_hold(&self, other: &Rectangle) -> bool {self.width > other.width && self.height > other.height}
}

示例11-5:使用第5章中的Rectangular结构及其can_hold方法

can_hold方法返回一个布尔值,这意味着它是assert!的完美用例。在示例11-6中,我们编写了一个测试,通过创建一个宽度为8、高度为7的Rectangular实例,并断言它可以容纳另一个宽度为5、高度为1的Rectangular实例,来练习can_hold方法。 

文件名:src/lib.rs

#[cfg(test)]
mod tests {use super::*;#[test]fn larger_can_hold_smaller() {let larger = Rectangle {width: 8,height: 7,};let smaller = Rectangle {width: 5,height: 1,};assert!(larger.can_hold(&smaller));}
}

 示例11-6:对can_hold的测试,检查一个较大的矩形是否可以容纳一个较小的矩形

注意,我们在tests模块中添加了新的一行:use super::*;tests模块是一个常规的模块,它遵循我们在第7章节中提到的常见的可见性规则。因为tests模块是一个内部模块,我们需要将外部模块中的测试代码放到内部模块的范围内。我们在这里使用了一个glob,所以我们在外部模块中定义的任何东西都可以用于这个tests模块。 

我们将我们的测试命名为llarger_can_hold_smaller,并且创建了我们需要的两个矩形实例。然后我们调用了assert!宏,并将调用larger.can_hold(&smaller)的结果传递给它。这个表达式应该返回true,所以我们的测试应该通过。让我们来了解一下!

$ cargo testCompiling rectangle v0.1.0 (file:///projects/rectangle)Finished test [unoptimized + debuginfo] target(s) in 0.66sRunning unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)running 1 test
test tests::larger_can_hold_smaller ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests rectanglerunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

 确实测试通过了!让我们添加另一个测试,这次断言较小的矩形不能容纳较大的矩形:

文件名:src/lib.rs

#[cfg(test)]
mod tests {use super::*;#[test]fn larger_can_hold_smaller() {// --snip--}#[test]fn smaller_cannot_hold_larger() {let larger = Rectangle {width: 8,height: 7,};let smaller = Rectangle {width: 5,height: 1,};assert!(!smaller.can_hold(&larger));}
}

 因为在这种情况下can_hold函数的正确结果是false,所以我们需要在将结果传递给assert!之前对其求反。因此,如果can_hold返回false,我们的测试将通过:

$ cargo testCompiling rectangle v0.1.0 (file:///projects/rectangle)Finished test [unoptimized + debuginfo] target(s) in 0.66sRunning unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests rectanglerunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

两项测试通过!现在让我们看看当我们在代码中引入一个bug时,测试结果会发生什么。我们将更改can_hold方法的实现,在比较宽度时用小于号替换大于号: 

// --snip--
impl Rectangle {fn can_hold(&self, other: &Rectangle) -> bool {self.width < other.width && self.height > other.height}
}

 运行测试现在会产生以下结果:

$ cargo testCompiling rectangle v0.1.0 (file:///projects/rectangle)Finished test [unoptimized + debuginfo] target(s) in 0.66sRunning unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... okfailures:---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::larger_can_hold_smallertest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass `--lib`

我们的测试发现了漏洞!因为larger.width是8,smaller.width是5,所以can_hold中的宽度比较现在返回false: 8不小于5。

用宏assert_eq!和assert_ne!测试相等性

验证功能的一种常见方法是测试被测代码的结果与您期望代码返回的值之间是否相等。您可以使用assert!来做到这一点,并使用==运算符向其传递一个表达式。然而,这是一个非常常见的测试,标准库提供了一对宏——assert_eq!assert_eq!—为了更方便地执行该测试。这些宏分别比较相等或不相等的两个参数。如果断言失败,它们还会打印这两个值,这更容易看出测试失败的原因;反之,assert!只指示它为==表达式获得了一个false,而不打印导致false的值。

在示例11-7中,我们编写了一个名为add_two的函数,将2加到它的参数中,然后我们使用assert_eq!测试这个函数。 

文件名:src/lib.rs

pub fn add_two(a: i32) -> i32 {a + 2
}#[cfg(test)]
mod tests {use super::*;#[test]fn it_adds_two() {assert_eq!(4, add_two(2));}
}

 示例11-7:使用assert_eq!测试函数add_two

让我们检查它是否通过! 

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.58sRunning unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::it_adds_two ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests adderrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

 我们将4作为参数传递给assert_eq!,等于调用add_two(2)的结果。这个测试的代码行是test tests::it_adds_two...okok文本表示我们的测试通过了!

让我们在代码中引入一个bug,看看assert_eq!是什么看起来当它失败时。将add_two函数的实现改为添加3:

pub fn add_two(a: i32) -> i32 {a + 3
}

再次运行测试:

$ cargo testCompiling adder v0.1.0 (file:///projects/adder)Finished test [unoptimized + debuginfo] target(s) in 0.61sRunning unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)running 1 test
test tests::it_adds_two ... FAILEDfailures:---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)`left: `4`,right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::it_adds_twotest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass `--lib`

我们的测试发现了bug!it_adds_two测试失败,消息告诉我们失败的断言是assertion failed: `(left == right)`以及left right值是什么。这条消息帮助我们开始调试:left的参数是4,但是right的参数是5,这里我们有add_two(2)。你可以想象,当我们有很多测试正在进行时,这将特别有帮助。

注意,在一些语言和测试框架中,等式断言函数的参数被称为expectedactual,我们指定参数的顺序很重要。然而,在Rust中,它们被称为leftright,我们指定我们期望的值和代码产生的值的顺序并不重要。我们可以把这个测试中的断言写成assert_eq!(add_two(2), 4),这将导致显示断言失败的相同assertion failed::`(left == right)`

assert_ne!如果我们给它的两个值不相等,将通过,如果相等,将失败。当我们不确定一个值是什么,但是我们知道这个值绝对不应该是什么时,这个宏是最有用的。例如,如果我们正在测试一个函数,它肯定会以某种方式改变它的输入,但是改变输入的方式取决于我们在一周中的哪一天运行测试,那么最好的断言可能是函数的输出不等于输入。

表面之下,是assert_eq!assert_ne!宏使用运算符==!=,分别为。当断言失败时,这些宏使用调试格式打印它们的参数,这意味着被比较的值必须实现PartialEqDebug特征。所有基本类型和大多数标准库类型都实现了这些特征。对于您自己定义的结构和枚举,您需要实现PartialEq来断言这些类型的相等性。当断言失败时,您还需要实现Debug来打印值。因为这两个特征都是可派生的特征,正如第5章示例5-12中提到的,这通常就像在你的结构或枚举定义中添加#[derive(PartialEq,Debug)]注释一样简单。请参阅附录C,“可衍生特征”,了解有关这些和其他可衍生特征的更多详细信息。

添加自定义失败消息

您还可以添加一个自定义消息,作为assert!的可选参数,与失败消息一起打印,assert_eq!,和assert_ne!。所需参数之后指定的任何参数都将传递给format!宏(在第8章的“用+运算符串联or格式!宏”部分),因此您可以传递一个包含{}占位符和要放入这些占位符的值的格式字符串。自定义消息对于记录断言的含义非常有用;当一个测试失败时,您会对代码的问题有更好的了解。

例如,假设我们有一个用名字问候别人的函数,我们想测试我们传递给函数的名字是否出现在输出中:

文件名:src/lib.rs 

pub fn greeting(name: &str) -> String {format!("Hello {}!", name)
}#[cfg(test)]
mod tests {use super::*;#[test]fn greeting_contains_name() {let result = greeting("Carol");assert!(result.contains("Carol"));}
}

 对这个程序的要求还没有达成一致,我们很确定开头的Hello文本会改变。我们决定,当需求改变时,我们不需要更新测试,所以我们不检查greeting函数返回的值是否完全相等,而是断言输出包含输入参数的文本。

现在让我们通过将greeting改为排除name来引入一个bug,看看默认测试失败是什么样子的:

pub fn greeting(name: &str) -> String {String::from("Hello!")
}

 运行该测试会产生以下结果:

$ cargo testCompiling greeter v0.1.0 (file:///projects/greeter)Finished test [unoptimized + debuginfo] target(s) in 0.91sRunning unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)running 1 test
test tests::greeting_contains_name ... FAILEDfailures:---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'assertion failed: result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::greeting_contains_nametest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass `--lib`

这个结果只是表明断言失败了,以及断言在哪一行。更有用的失败消息是打印greeting函数的值。让我们添加一个定制的失败消息,该消息由一个格式字符串组成,该格式字符串带有一个占位符,占位符中填充了我们从greeting函数中获得的实际值:

    #[test]fn greeting_contains_name() {let result = greeting("Carol");assert!(result.contains("Carol"),"Greeting did not contain name, value was `{}`",result);}

现在,当我们运行测试时,我们将得到一个更具信息性的错误消息:

$ cargo testCompiling greeter v0.1.0 (file:///projects/greeter)Finished test [unoptimized + debuginfo] target(s) in 0.93sRunning unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)running 1 test
test tests::greeting_contains_name ... FAILEDfailures:---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracefailures:tests::greeting_contains_nametest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass `--lib`

 我们可以在测试输出中看到我们实际得到的值,这将帮助我们调试发生了什么,而不是我们预期会发生什么。

使用should_panic检查panic

除了检查返回值,检查我们的代码是否如我们所期望的那样处理错误情况也很重要。例如,考虑我们在第9章示例9-13中创建的猜测类型。其他使用Guess的代码依赖于Guess实例只包含1到100之间的值的保证。我们可以编写一个测试,确保试图创建一个值在该范围之外的Guess实例时会出错。

我们通过向测试函数添加属性should_panic来实现这一点。如果函数内部的代码出现混乱,则测试通过;如果函数内部的代码没有死机,测试就会失败。

示例11-8显示了一个测试,它检查Guess::new的错误条件是否在我们期望的时候发生。

文件名:src/lib.rs

pub struct Guess {value: i32,
}impl Guess {pub fn new(value: i32) -> Guess {if value < 1 || value > 100 {panic!("Guess value must be between 1 and 100, got {}.", value);}Guess { value }}
}#[cfg(test)]
mod tests {use super::*;#[test]#[should_panic]fn greater_than_100() {Guess::new(200);}
}

 示例11-8:测试一个条件会导致一个panic!

我们将#[should_panic]属性放在#[test]属性之后,它所应用的测试函数之前。让我们看看测试通过后的结果: 

$ cargo testCompiling guessing_game v0.1.0 (file:///projects/guessing_game)Finished test [unoptimized + debuginfo] target(s) in 0.58sRunning unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)running 1 test
test tests::greater_than_100 - should panic ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests guessing_gamerunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

看起来不错!现在,让我们在代码中引入一个错误,删除如果值大于100,new将会死机的条件: 

// --snip--
impl Guess {pub fn new(value: i32) -> Guess {if value < 1 {panic!("Guess value must be between 1 and 100, got {}.", value);}Guess { value }}
}

当我们运行示例11-8中的测试时,它会失败:

$ cargo testCompiling guessing_game v0.1.0 (file:///projects/guessing_game)Finished test [unoptimized + debuginfo] target(s) in 0.62sRunning unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)running 1 test
test tests::greater_than_100 - should panic ... FAILEDfailures:---- tests::greater_than_100 stdout ----
note: test did not panic as expectedfailures:tests::greater_than_100test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass `--lib`

 在这种情况下,我们没有得到非常有用的消息,但是当我们查看测试函数时,我们看到它被注释为#[should_panic]。我们得到的失败意味着测试函数中的代码没有导致死机。

使用should_panic的测试可能不精确。should_panic测试将会通过,即使测试因不同于我们预期的原因而死机。为了使should_panic测试更加精确,我们可以向should_panic属性添加一个可选的预期参数。测试工具将确保失败消息包含所提供的文本。例如,考虑示例11-9中Guess的修改代码,其中new根据值是太小还是太大而出现不同的消息。

文件名:src/lib.rs

// --snip--impl Guess {pub fn new(value: i32) -> Guess {if value < 1 {panic!("Guess value must be greater than or equal to 1, got {}.",value);} else if value > 100 {panic!("Guess value must be less than or equal to 100, got {}.",value);}Guess { value }}
}#[cfg(test)]
mod tests {use super::*;#[test]#[should_panic(expected = "less than or equal to 100")]fn greater_than_100() {Guess::new(200);}
}

 示例11-9:测试panic!带有包含指定子字符串的紧急消息

这个测试将会通过,因为我们在should_panic属性的expected参数中输入的值是Guess::new函数出错的消息的子字符串。我们可以指定我们期望的整个紧急消息,在本例中,Guess value must be less than or equal to 100, got 200.。您选择指定的内容取决于恐慌消息中有多少是独特的或动态的,以及您希望测试有多精确。在这种情况下,紧急消息的子字符串足以确保测试函数中的代码执行else if value > 100的情况。 

为了查看当带有expected消息的should_panic测试失败时会发生什么,让我们通过交换if value < 1else if value > 100块的主体,再次在代码中引入一个bug:

        if value < 1 {panic!("Guess value must be less than or equal to 100, got {}.",value);} else if value > 100 {panic!("Guess value must be greater than or equal to 1, got {}.",value);}

这一次,当我们运行should_panic测试时,它将失败:

$ cargo testCompiling guessing_game v0.1.0 (file:///projects/guessing_game)Finished test [unoptimized + debuginfo] target(s) in 0.66sRunning unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)running 1 test
test tests::greater_than_100 - should panic ... FAILEDfailures:---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected stringpanic message: `"Guess value must be greater than or equal to 1, got 200."`,expected substring: `"less than or equal to 100"`failures:tests::greater_than_100test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: test failed, to rerun pass `--lib`

失败消息表明该测试确实如我们预期的Result<T,E >那样死机,但是死机消息不包括预期的字符串'Guess value must be less than or equal to 100'。在这种情况下,我们得到的紧急消息是Guess value must be greater than or equal to 1, got 200。现在我们可以开始找出我们的错误在哪里了!

在测试中使用Result< T,E >

到目前为止,我们的测试失败时都会惊慌失措。我们也可以编写使用Result<T,E >的测试!下面是示例11-1中的测试,重写后使用Result<T,E >并返回一个Err而不是死机:

#[cfg(test)]
mod tests {#[test]fn it_works() -> Result<(), String> {if 2 + 2 == 4 {Ok(())} else {Err(String::from("two plus two does not equal four"))}}
}

it_works函数现在有了Result <(),String >返回类型。在函数体中,而不是调用assert_eq!,当测试通过时,我们返回Ok(()),当测试失败时,返回一个包含StringErr

编写返回Result<T,E >的测试使您能够在测试体中使用问号操作符,这是一种编写测试的便捷方式,如果测试中的任何操作返回Err变量,测试就会失败。

不能在使用Result<T,E >的测试上使用#[should_panic]批注。要断言一个操作返回一个EErrrr变量,不要在Result<T,E >值上使用问号运算符。而是使用assert!(value.is_err())。 

现在您已经知道了编写测试的几种方法,让我们看看运行测试时会发生什么,并探索我们可以使用cargo test的不同选项。 

本章重点

  • 自动化测试的概念
  • 如何编写测试用例
  • assert!在测试用例中如何使用
  • assert_eq!和assert_ne!在测试用例中如何使用
  • 添加自定义失败信息
  • 使用should_panic检查panic
  • 使用Result<T, E>

相关文章:

Rust之自动化测试(一):如何编写测试

开发环境 Windows 10Rust 1.71.1 VS Code 1.81.1 项目工程 这里继续沿用上次工程rust-demo 编写自动化测试 Edsger W. Dijkstra在他1972年的文章《谦逊的程序员》中说&#xff0c;“程序测试可以是一种非常有效的方法来显示错误的存在&#xff0c;但它对于显示它们的不存在…...

简单聊聊Https的来龙去脉

简单聊聊Https的来龙去脉 Http 通信具有哪些风险Https Http SSL/TLS对称加密 和 非对称加密数字证书数字证书的申请数字证书怎么起作用 Https工作流程一定需要Https吗&#xff1f; Http 通信具有哪些风险 使用明文通信&#xff0c;通信内容可能会被监听不验证通信双方身份&a…...

【注册岩土】Python土力学与基础工程计算.PDF-土中的应力

Python 求解代码如下&#xff1a; 1&#xff0e;&#xff03;计算竖向有效自重应力2.h12#m3.h21.5#m4.h31#m5.gamma1 19# kN/m^36.gamma218# kN/m^37.gamma317# kN/m^38.sigma_c gammal * h1 gamma2*h2 gamma3 *h39&#xff0e;print&#xff08;&#xff02;竖向有效自重应力…...

祝贺!Databend Cloud 和阿里云 PolarDB 达成认证

近日&#xff0c;北京数变科技有限公司旗下产品与阿里云 PolarDB 开源数据库社区展开产品集成认证。 测试结果表明&#xff0c;北京数变科技有限公司旗下产品《Databend Cloud&#xff08;V1.25&#xff09;》正式通过了《阿里云 PolarDB 数据库管理软件》的技术认证&#xff…...

SQL语言-01

SQL Structured Query Language 的简单介绍 SQL 中的书写规则 SQL 中的数据类型...

PyCharm软件安装包分享(附安装教程)

目录 一、软件简介 二、软件下载 一、软件简介 PyCharm是一种集成开发环境&#xff08;IDE&#xff09;&#xff0c;专门为Python开发者设计。它是由捷克软件公司JetBrains开发的&#xff0c;为Python开发人员提供了高效、易用和功能丰富的工具集。 以下是PyCharm软件的主要…...

AI文本标注的概念,类型和方法

我们每天都在与不同的媒介&#xff08;例如文本、音频、图像和视频&#xff09;交互&#xff0c;我们的大脑对收集到的信息进行处理和加工&#xff0c;从而指导我们的行为。在我们日常接触到的信息中&#xff0c;文本是最常见的媒体类型之一&#xff0c;由我们交流使用的语言构…...

【AutoLayout案例04-游戏图片-按钮适配 Objective-C语言】

一、好,我们再看一个案例, 刚才,这个案例, 这么一个案例 这个案例,是什么意思呢, 这里给大家做一个3.5英寸、4.0英寸的屏幕适配, 因为我们这里图片,只有一个,就是4英寸的这么一个图片 什么意思呢,要求我们在3.5英寸的屏幕、和4英寸的屏幕的时候,都能正常显示这个图…...

Spring Boot业务系统如何实现海量数据高效实时搜索

1.概述 我们都知道随着业务系统的发展和使用&#xff0c;数据库存储的业务数据量会越来越大&#xff0c;逐渐成为了业务系统的瓶颈。在阿里巴巴开发手册中也建议&#xff1a;单表行数超过500万行或者单表容量超过2GB才推荐进行分库分表&#xff0c;如果预计三年后数据量根本达…...

面向对象的设计原则

设计模式 Python 设计模式&#xff1a;对软件设计中普遍存在&#xff08;反复出现&#xff09;的各种问题&#xff0c;所提出的解决方案。每一个设计模式系统地命名、解释和评价了面向对象系统中一个重要的和重复出现的设计 面向对象 三大特性&#xff1a;封装、继承、多态 …...

前端需要理解的工程化知识

1 Git 1.1 Git 常见工作流程 Git 有4个区域&#xff1a;工作区&#xff08;workspace)、index&#xff08;暂存区&#xff09;、repository&#xff08;本地仓库&#xff09;和remote&#xff08;远程仓库&#xff09;&#xff0c;而工作区就是指对文件发生更改的地方&#xff…...

【Terraform学习】使用 Terraform创建DynamoDB添加项目(Terraform-AWS最佳实战学习)

本站以分享各种运维经验和运维所需要的技能为主 《python》&#xff1a;python零基础入门学习 《shell》&#xff1a;shell学习 《terraform》持续更新中&#xff1a;terraform_Aws学习零基础入门到最佳实战 《k8》暂未更新 《docker学习》暂未更新 《ceph学习》ceph日常问题解…...

基于单片机教室人数实时检测系统

一、系统方案 主程序中main函数主要是引脚的初始化&#xff0c;给单片机引脚初始化&#xff0c;初始化LCD1602&#xff0c;初始化红外对管&#xff0c;通过对LCD1602赋值&#xff0c;采集进入教室的人数&#xff0c;显示在LCD1602上面进出人数我们采用按键的形式&#xff0c;检…...

alibabacloud的简单使用,nacos配置中心+服务中心。作者直接给自己写的源码

文章目录 依赖关键主要的程序启动文件配置文件bootstrap.yml依赖文件nacos配置中心上的文件截图 启动成功截图参考文档 依赖关键 SpringBoot版本和com.alibaba.cloud版本需要对应&#xff0c;不然会程序会启动失败作者使用的版本 SpringBoot: 2.1.6.RELEASE alibabacloud: 2.…...

Python爬虫:一个爬取豆瓣电影人像的小案例

从谷歌浏览器的开发工具进入 选择图片右键点击检查 ![在这里插入图片描述](https://img-blog.csdnimg.cn/1b38c2a942c441fb8cb545a28bb35015.png 翻页之后发现网址变化的只有start数值&#xff0c;每次变化值为30 Python代码 import requests from bs4 import BeautifulSou…...

STM32CubeMX配置STM32G0 Standby模式停止IWDG(HAL库开发)

1.打开STM32CubeMX选择好对应的芯片&#xff0c;打开IWDG 2.打开串口1进行调试 3.配置好时钟 4.写好项目名称&#xff0c;选好开发环境&#xff0c;最后获取代码。 5.打开工程&#xff0c;点击魔术棒&#xff0c;勾选Use Micro LIB 6.修改main.c #include "main.h"…...

39.RESTful案例

RESTful案例 准备环境 Employee.java public class Employee {private Integer id;private String lastName;private String email;//1 male, 0 femaleprivate Integer gender; } //省略get、set和构造方法EmployeeDao.java package com.atguigu.SpringMVC.dao;import com.…...

Power Pivot 实现数据建模

一、简介 Excel中的透视表适合小规模数据&#xff1b;如果想在稍微大一些的数据中进行高性能透视表分析&#xff0c;就要使用Power Pivot&#xff1b;再大一些数据&#xff0c;可能就需要大数据分析服务来进行分析。 Power Pivot&#xff0c;可以让没有技术背景的企业业务人员…...

Ansible自动化运维之playbooks剧本

文章目录 一.playbooks介绍1.playbooks简述2.playbooks剧本格式3.playbooks组成部分4.运行playbooks及检测文件配置 二.模块实战实例1.playbooks模块实战实例2.vars模块实战实例3.指定远程主机sudo切换用户4.when模块实战实例5.with_items迭代模块实战实例6.Templates 模块实战…...

Docker - Docker安装MySql并启动

因为项目需要连接数据库&#xff0c;但是远程服务器上的mysql我不知道账户和密码&#xff0c;这个时候便是docker发挥作用的关键时刻了&#xff01; 目录 docker安装安装gcc卸载老docker&#xff08;如有&#xff09;安装软件包设置镜像仓库更新yum软件包索引安装docker启动doc…...

国防科技大学计算机基础课程笔记02信息编码

1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制&#xff0c;因此这个了16进制的数据既可以翻译成为这个机器码&#xff0c;也可以翻译成为这个国标码&#xff0c;所以这个时候很容易会出现这个歧义的情况&#xff1b; 因此&#xff0c;我们的这个国…...

从WWDC看苹果产品发展的规律

WWDC 是苹果公司一年一度面向全球开发者的盛会&#xff0c;其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具&#xff0c;对过去十年 WWDC 主题演讲内容进行了系统化分析&#xff0c;形成了这份…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例

文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

C# 类和继承(抽象类)

抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storms…...

vue3 daterange正则踩坑

<el-form-item label"空置时间" prop"vacantTime"> <el-date-picker v-model"form.vacantTime" type"daterange" start-placeholder"开始日期" end-placeholder"结束日期" clearable :editable"fal…...

C++_哈希表

本篇文章是对C学习的哈希表部分的学习分享 相信一定会对你有所帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、基础概念 1. 哈希核心思想&#xff1a; 哈希函数的作用&#xff1a;通过此函数建立一个Key与存储位置之间的映射关系。理想目标&#xff1a;实现…...

解析“道作为序位生成器”的核心原理

解析“道作为序位生成器”的核心原理 以下完整展开道函数的零点调控机制&#xff0c;重点解析"道作为序位生成器"的核心原理与实现框架&#xff1a; 一、道函数的零点调控机制 1. 道作为序位生成器 道在认知坐标系$(x_{\text{物}}, y_{\text{意}}, z_{\text{文}}…...