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

Rust 自动化测试框架 Rust和Python测试框架对比摘要 Rust提供轻量级内置测试框架,通过#[test]属性和cargo test命令运行,支持单元测试、集成测试和文档测试。其测试编译为原生代码,执行速度快,线程隔离性好,但功能较为基础,复杂场景需依赖第三方库如proptest和mockall。 Python测试生态成熟,pytest为主流框架,支持fixtures、参数化测试等丰富功能,断言使用Python原生语法,调试方便。虽运行时性能不及Rust,但插件体系完善,适合快速迭代开发。 关键差异: 性能: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 --lib
     Created 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 test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test 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 test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.59s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::exploration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test 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 test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.72s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- 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 backtrace


failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: 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 test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 1 test
test tests::larger_can_hold_smaller ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test 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 test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running 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 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test 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 test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running 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 ... ok

failures:

---- 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 backtrace


failures:
    tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: 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 test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test 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...ok,ok文本表示我们的测试通过了!

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

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

再次运行测试:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- 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 backtrace


failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

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

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

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

表面之下,是assert_eq!和assert_ne!宏使用运算符==和!=,分别为。当断言失败时,这些宏使用调试格式打印它们的参数,这意味着被比较的值必须实现PartialEq和Debug特征。所有基本类型和大多数标准库类型都实现了这些特征。对于您自己定义的结构和枚举,您需要实现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 test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- 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 backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: 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 test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- 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 backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: 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 test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests guessing_game

running 0 tests

test 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 test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: 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 < 1和else 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 test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- 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 string
      panic 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_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: 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(()),当测试失败时,返回一个包含String的Err。

编写返回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自动化测试的框架 摘要:本文介绍了使用Rust实现Web自动化测试的多种框架和方案。主要包括基于Selenium的thirtyfour框架、无浏览器模式的headless_chrome、移动端测试的Appium客户端,以及结合Django和Postman的测试方法。文章还涵盖了API测试、性能测试、多浏览器并行测试、视觉验证等场景,展示了Rust在自动化测试领域的多样化应用。关键实现包括测试框架集成、断言验证、异常处理、并发测试和报告生成等技术要点,为不同测试需求提供了完整的Rust解决方案。 阅读详情

相关推荐

在Rust中编写自动化测试

Rust中的测试函数是用来验证非测试代码是否是按照期望的方式运行的, 测试函数体通常需要执行三种操作:1.设置任何所需的数据或状态;2.运行需要测试的代码;3.断言其结果是我们所期望的。本篇文章主要探讨了Rust自动化测试的几种常见场景。

这里是二进制空间安全博客,主要分享网络安全、信息安全和数据安全相关的技术文章。内容覆盖编程语言、基础知识、漏洞分析、攻防技巧、安全工具使用、最佳实践等安全技术主题。 1659

【Rust 基础篇】Rust 测试详解

本篇博客详细介绍了 Rust 中的测试,包括测试函数的编写、测试断言、测试组织以及测试覆盖率。通过充分利用 Rust 的测试框架和工具,我们可以编写高质量、可靠的 Rust 代码。希望本篇博客对你理解和应用 Rust 中的测试有所帮助。

繁依Fanyi的博客 2088

【Rust测试】Rust代码测试方法详解与应用实战

测试是软件开发中不可或缺的重要环节,它能够确保代码质量、减少bug并提高软件的可靠性。 Rust语言从设计之初就内置了对测试的支持,提供了完善的测试框架和工具链。 本文将全面介绍Rust中的测试方法,从基础概念到高级技巧,并结合实际案例展示如何在Rust项目中实施有效的测试策略。

景天科技苑 4万+

【RUST学习】测试

可以向assert!、assert_eq!、assert_ne!添加可选的自定义消息—这些自定义消息和失败消息都会打印出来—assert!:第一参数必填,自定义消息作为第二个参数。—assert_eq!和assert_ne!: 前两个参数必填,自定义消息作为第三个参数。—自定义消息参数会被传递给format!宏,可以使用{}占位符format!("hello {}!result。

qq_54384621的博客 1961

Open3D:Win10 + VS2017配置Open3D(C++、python)

一、下载源码 地址:https://github.com/intel-isl/Open3D 1.1 克隆源码【笔者的源码路径为D:\Open3D\Open3D】 1.2 切换版本【笔者的版本为0.8.0】 1.3 更新子模块【用于获取第三方库!很重要!】 二、CMake编译 2.1 设置路径 【source code: D:\Open3D\Open3D;build binaries: D:\Open3D】 【笔者设置CMAKE/CMAKE_INSTALL_PREFIX的路径为D:/Open.

累了就要打游戏 1万+

Godot 2D游戏开发实战:用GDScript复刻10款经典小游戏(附完整源码)

本文是一份Godot 2D游戏开发实战指南,通过使用GDScript语言复刻10款经典小游戏,帮助开发者快速掌握引擎核心功能。内容涵盖从环境配置、GDScript基础到贪吃蛇、Flappy Bird、打砖块、俄罗斯方块等项目的核心逻辑与代码实现,并附有完整源码,旨在通过实践提升独立开发能力。

gold的博客 778

Rust单元测试、集成测试

Rust 提供了单元测试和集成测试两种方式来帮助我们组织测试代码以解决代码正确性问题。单元测试的模块和待测试的代码在同一个文件中,且可以很方便地对私有函数进行测试集成测试文件放在项目根目录下的tests目录中,由于该目录下每个文件都是一个包,我们必须要引入待测试的代码到当前包的作用域中,才能进行测试,正因为此,集成测试只能对声明为pub的 API 进行测试下个章节,我们再来看看该如何使用对 Rust 项目进行持续集成。ripgrep。

m0_52796585的博客 2388

Carla中如何渲染opendrive车道线和车道标识(全网独家,附代码)

carla是可以单独加载opendrive并渲染路面的,但是无法把车道线渲染出来,这是一个大坑,因为没有车道线就无法给感知输入。楼主已经实现,直接渲染车道线或者带PBR贴图的车道标识。实现起来非常简单,但必须对carla源码以及ue的开发足够了解。在carla源码中有一个很重要的类opendrivegenerator,它是关于opendrive渲染的全部方法,但是你是找不到它实例化的地方的,因为它被定义在蓝图关卡中。网上包括carla官方关于carla源码的分析少之又少,楼主将自己的经验写出来供大家参考。

u013998156的博客 1090

rust的测试

一、测试 软件测试越来越受到所有开发人员的重视,当然,很多人也不重视。只是相对于以前来说。至少,现在技术型公司都安排了测试部门,以前可都是开发人员自己测试一下就OK了。Edsger W. Dijkstra 说“Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.”可见软件测试多么重要又多么让人绝望。

fpcc的专栏 1567

基于Simulink的光伏电站AGC/AVC协调控制仿真

摘要:本文基于Simulink构建光伏电站AGC/AVC协调控制仿真模型,通过分层控制架构实现有功-无功解耦与储能协同。系统包含100kW光伏阵列、50kWh储能及逆变器组件,采用PID控制器实现AGC频率调节(±0.1Hz)和Q-V下垂控制实现AVC电压稳定(±2%)。仿真验证了三种工况下协调控制的有效性:正常调度时储能充电20kW;负荷突增时储能15kW放电维持频率;光照骤降时储能30kW放电并吸收2kVAR无功稳定电压。结果表明,该方案可使频率/电压偏差分别控制在0.1Hz和7.6V内,响应时间&lt

xiaoheshang_123的博客 235

【AI应用实战-WorkBuddy】WorkBuddy 能帮你做什么(一)

文章摘要: WorkBuddy是一款AI工作助手,专为提升效率而设计,适用于经常处理文档、代码、数据分析等重复性任务的职场人士。它能通过自然语言交互快速完成五大核心场景任务: 周报撰写:输入工作要点,10分钟生成专业周报; 代码调试:解析报错信息,3分钟提供解决方案; Excel分析:自动计算数据趋势并生成图表和结论; 自动化脚本:将重复流程转为脚本,节省98%时间; 文档生成:根据代码自动补充README、API文档和注释。 效果对比:传统手动操作耗时1-3小时的任务,WorkBuddy可将时间缩短至10

348

【Rust】——编写自动化测试(一)

🎯编写和运行测试 🎃测试(函数) 🎃解剖测试函数 🎯 断言(Assert) 🎃使用assert!宏检查测试结果 🎃使用assert_eq!和assert_ne!测试相等性 🎯自定义错误信息 🎯使用should_panic检查恐慌 🎃让should_panic更加精准 🎯在测试中使用Result

shsjssnn的博客 3680

SAP_SD模块路径及运输功能后台配置简介

后台路径:SPRO—后勤执行—装运—基本发运功能—定义路线—定义传输模式根据实际运输过程运输地理的类型:海运、公路运输、空运后台路径:SPRO—后勤执行—装运—基本发运功能—定义路线—定义发运类型根据实际运输过程运输方式:汽车、卡车、火车、轮船、飞机后台路径:SPRO—后勤执行—装运—基本发运功能—路径—定义路线—定义运输连接点指在运输过程中需要中转的地区可以维护中转地区的停留的时间后台路径:SPRO—后勤执行—装运—基本发运功能—路径—定义路线—定义路径和阶段。

我小时候很黑的博客 1396

Rust 学习笔记:编写自动化测试

Rust 学习笔记:编写自动化测试

ProgramNovice的博客 1278

微信商家转账报错“当前使用用户过多,请稍后重新确认收款”解决方案

当前使用用户过多,请稍后重新确认收款”这个错误,其实就是典型的参数误处理导致的假报错。记住一句话就行👇JSAPI 场景下的package参数不要做 UrlEncode。如果你也遇到这个问题,希望这篇文章能帮你少踩一个坑。

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 784

【中科院一区Top-源码复现】Day-ahead photovoltaic power forecastingwith multi-source temporal-featureconvoluti

【中科院一区Top-源码复现】Day-ahead photovoltaic power forecastingwith multi-source temporal-featureconvoluti

739

Rust语言的自动化测试

Rust是一种系统编程语言,兼具高效性和内存安全性。Rust通过独特的所有权系统提供内存安全,而无需垃圾回收。这使得Rust适合用于开发高性能和安全性要求高的应用程序,比如操作系统、游戏引擎和网络服务。Rust的特点包括:内存安全:通过所有权和生命周期机制,Rust能够在编译期间确保程序的内存安全。高性能:Rust的性能接近C/C++,在许多场景下可以和这些语言媲美。并发模型:Rust通过所有权模型,使得并发编程更加简单,避免数据竞争问题。

2501_91503265的博客 868

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

《Rust权威指南》读书笔记10 - 编写自动化测试

测试是保障程序正确性的重要手段。功能上的漏洞不能直接被编译器检测和捕获,必须通过执行一些测试例并比较期望结果得出。手动编写测试用例是一个较为冗长繁琐的过程,Rust提供了一些自动化测试方法,能够标准化、高效化进行功能的自动测试。

Zheng__Huang的博客 1521

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Rust权威指南之编写自动化测试

本文来源于阅读《Rust权威指南》第十一章:编写自动化测试的读书笔记。快速了解Rust中关于编写单元测试和集成测试的相关知识。

学习和工作总结,共勉! 1393

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

【Rust】——编写自动化测试

🎯控制测试如何运行 🎃并行或连续的运行测试 🎃显示函数输出 🎯按测试的名称进行测试 🎯忽略测试

shsjssnn的博客 1672

【跟小嘉学 Rust 编程】十一、编写自动化测试

程序测试是一种非常有效方法来测试Bug的存在,但是对于显示它们的缺失是完全不够的。程序的正确性是指代码在多大程度上完成了我们想让它做的事情。Rust 在设计时高度关注程序的正确性,但正确性时复杂的,而且不容易证明。本章节将会讲解测试函数、单元测试、集成测试、性能测试等等。主要教材参考 《The Rust Programming Language》

小嘉丶学长的博客 1768

《Rust 程序设计语言》第十一章:编写自动化测试

本文介绍了Rust中的自动化测试方法。主要内容包括: 测试基础:程序测试可以发现问题但不能保证没有bug,Rust通过类型系统和自动化测试来确保程序正确性。 测试编写:使用#[test]属性标记测试函数,通过assert!、assert_eq!等宏验证结果。测试通常包含三个步骤:准备数据、执行代码、验证结果。 测试控制:可以并行或串行运行测试,显示/隐藏输出,按名称筛选测试,忽略特定测试等。 测试组织: 单元测试:放在src目录下,测试单个模块,可以测试私有函数 集成测试:放在tests目录下,测试公共AP

北京锋通科技有限公司的技术blog 1117

编写自动化测试(11)

任何在assert!的一个必需参数和assert_eq!和assert_ne!的两个必需参数之后指定的参数都会传递给format!宏format!("Hello {}!result。

菜鸡,轻喷 1502

Rust之编写自动化测试

在最简单的情形下,Rust中的测试就是一个标注有test属性的函数。属性 (attribute)是一种用于修饰Rust代码的元数据。只需要将#[test]添加到关键字fn的上一行便可以将函数转变为测试函数。当测试编写完成后,我们可以使用cargo test命令来运行测试。这个命令会构建并执行一个用于测试的可执行文件,该文件在执行的过程中会逐一调用所有标注了test属性的函数,并生成统计测试运行成功或失败的相关报告。

a_qwq_a的博客 1828

Rust学习记录--C11 编写自动化测试

可以向assert!assert_eq!assert_ne!添加可选的自定义消息这些自定义消息和失败消息都会打印出来assert!:第1个参数必填,自定义消息作为第2个参数assert_eq!assert_ne!:前两个参数必填,自定义消息作为第3个参数自定义消息参数会被传递给format!宏,可以使用占位符{}例子format!

weixin_44900096的博客 865
上一篇: Rust之泛型、特性和生命期(三):验证有生存期的引用
下一篇: Rust之自动化测试(二):控制测试如何运行
枫★曦
博客等级 码龄19年 251粉丝 153原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值