如何编写测试


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


  • 设置任何需要的数据或状态。

  • 运行要测试的代码。

  • 断言结果是您所期望的。


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


测试函数剖析


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


每当我们使用 Cargo 创建新的库项目时,都会自动生成一个带有测试函数的测试模块。此模块为您提供了一个用于编写测试的模板,因此您不必在每次开始新项目时都查找确切的结构和语法。您可以根据需要添加任意数量的附加测试函数和测试模块!


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


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

$ cargo new adder --lib
     Created library `adder` project
$ cd adder


你的加法器库中 src/lib.rs 文件的内容应该看起来像示例 11-1。


文件名: src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

示例 11-1:cargo new 自动生成的代码


现在,我们只关注 it_works 函数。注意 #[test] annotation:此属性表示这是一个测试函数,因此 test Runner 知道将此函数视为测试。我们也可能有 non-test functions 来帮助设置常见场景或执行常见作,因此我们始终需要指出哪些函数是 Test。


示例函数体使用 assert_eq! 宏来断言该结果(包含 2 和 2 相加的结果)等于 4。此断言用作典型测试的格式示例。让我们运行它以查看此测试是否通过。


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

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [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 编译并运行了测试。我们看到运行 1 次测试的行。下一行显示生成的测试函数的名称,称为 tests::it_works,并且运行该测试的结果是正常的。总体摘要测试结果:ok. 表示所有测试都已通过,显示 1 passed; 0 failed 的部分表示通过或失败的测试总数。


可以将测试标记为 ignored,以便它不会在特定实例中运行;我们将在“忽略某些测试,除非特别 requested“部分。因为我们在这里没有这样做,所以摘要显示 0 ignored


测量的 0 统计量用于测量性能的基准测试。 在撰写本文时,基准测试仅在 nightly Rust 中可用。看 有关基准测试的文档以了解更多信息。


我们可以向 cargo test 命令传递一个参数,以仅运行名称与字符串匹配的测试;这称为 过滤,我们将在 “按名称运行测试子集”部分。在这里,我们没有筛选正在运行的测试,因此摘要的末尾显示 0 filtered out


Doc-tests adder 开始的测试输出的下一部分是针对任何文档测试的结果。我们还没有任何文档测试,但 Rust 可以编译出现在我们 API 文档中的任何代码示例。此功能有助于使您的文档和代码保持同步!我们将在 “Documentation Comments as Tests“部分。现在,我们将忽略 Doc-tests 输出。


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


文件名: src/lib.rs

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}


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

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [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


现在我们将添加另一个测试,但这次我们将进行一个失败的测试!当测试函数中的某些内容出现 panic 时,测试将失败。每个测试都在新线程中运行,当主线程看到测试线程已死亡时,测试将标记为失败。在第 9 章中,我们讨论了 panic 的最简单方法是如何调用 panic!宏。将新测试作为名为 另一个,所以你的 src/lib.rs 文件看起来像示例 11-3。


文件名: src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

示例 11-3:添加第二个测试将失败,因为我们调用 panic!


使用 cargo test 再次运行测试。输出应该看起来像示例 11-4,它显示我们的探索测试通过,另一个测试失败。

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [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 src/lib.rs:17:9:
Make this test fail
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:一个测试通过和一个测试失败时的测试结果


测试 tests::another 显示 FAILED,而不是 ok。在单个结果和摘要之间显示两个新部分:第一个部分显示每个测试失败的详细原因。在本例中,我们获得了另一个失败的详细信息,因为它 panicked at 'Make this test fail' 位于 src/lib.rs 文件中的第 17 行。下一节仅列出所有失败测试的名称,这在存在大量测试和大量详细的失败测试输出时非常有用。我们可以使用失败测试的名称来运行该测试,以便更轻松地调试它;我们将在 “控制测试的运行方式” 一节中详细讨论运行测试的方法。


摘要行显示在末尾:总体而言,我们的测试结果为 FAILED。我们有一次测试通过,一次测试失败。


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


使用 assert!


当您想要确保测试中的某些条件的计算结果为 true 时,标准库提供的 assert! 宏非常有用。我们给出 断言!macro 是一个计算结果为 Boolean 的参数。如果值为 true,则不会发生任何作,并且测试通过。如果值为 false,则 断言!宏调用 panic!,导致测试失败。使用 assert! macro 帮助我们检查代码是否按预期运行。


在第 5 章示例 5-15 中,我们使用了一个 Rectangle 结构体和一个 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 章中的 Rectangle 结构体及其 can_hold 方法


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


文件名: 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
    }
}

#[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,所以我们在外部模块中定义的任何东西都可以用于 this tests 模块。


我们已将测试larger_can_hold_smaller命名,并创建了两个 Rectangle 实例。然后我们调用 assert!宏,并将调用 larger.can_hold(&smaller) 的结果传递给它。此表达式应返回 true,因此我们的测试应通过。让我们来了解一下!

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [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

#[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
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        // --snip--
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[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` profile [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 方法,将大于号替换为小于号 比较宽度:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

#[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));
    }

    #[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));
    }
}


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

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [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 src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
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.width8,而 smaller.width5 中,can_hold 中的宽度比较现在返回 false:8 不小于 5。


使用 assert_eq!assert_ne!


验证功能的一种常见方法是测试待测代码的结果与您期望代码返回的值之间的相等性。为此,您可以使用 assert!宏,并使用 == 运算符。但是,这是一个非常常见的测试,因此标准库提供了一对宏 — assert_eq!assert_ne!— 以更方便地执行此测试。这些宏分别比较相等或不相等的两个参数。如果断言失败,它们还会打印这两个值,这样可以更轻松地查看测试失败的原因;相反, 断言!macro 仅表示它为 == 获取了 false 值 expression,而不打印导致 false 值的值。


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


文件名: src/lib.rs
pub fn add_two(a: usize) -> usize {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}

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


让我们检查一下它是否通过!

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [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


我们创建一个名为 result 的变量,它保存调用 add_two(2)。然后我们将 result4 作为参数传递给 assert_eq!。此测试的输出行为 test tests::it_adds_two ... ok并且 ok text 表示我们的测试通过!


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

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}


再次运行测试:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [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 src/lib.rs:12:9:
assertion `left == right` failed
  left: 5
 right: 4
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`


我们的测试发现了错误!it_adds_two 测试失败,该消息告诉我们 left assertion `left == right` failedright 值是什么。此消息有助于我们开始调试:调用 add_two(2) 的结果所在的左侧参数为 5,但右侧参数为 4。您可以想象,当我们进行大量测试时,这将特别有用。


请注意,在某些语言和测试框架中,相等断言函数的参数称为 expectedactual,我们指定参数的顺序很重要。然而,在 Rust 中,它们被称为 leftright 的 intent 和我们指定期望的值和值的顺序 代码生成无关紧要。我们可以将此测试中的断言写为 assert_eq!(4, result) 的 Alpha S Package,这将产生与显示 assertion failed: `(left == right)` 相同的失败消息。


如果我们给它的两个值不相等,则 assert_ne! 宏将传递,如果它们相等,则失败。这个宏在我们不确定值是什么,但我们知道该值绝对不应该是什么的情况下最有用。例如,如果我们正在测试一个保证以某种方式更改其输入的函数,但更改输入的方式取决于我们运行测试的星期几,那么最好的断言可能是函数的输出不等于输入。


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


添加自定义失败消息


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


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


文件名: 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 函数,我们只需断言输出包含 input 参数的文本。


现在让我们通过将 greeting 更改为 exclude 来引入此代码中的 bug name 查看默认测试失败的情况:

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}


运行此测试将生成以下内容:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [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 src/lib.rs:12:9:
assertion failed: result.contains("Carol")
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 函数。让我们添加一条由格式 string 中,其中的占位符填充了我们从 greeting 函数:

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

#[cfg(test)]
mod tests {
    use super::*;

    #[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` profile [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 src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
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 的其他代码 取决于 Guess 实例将仅包含值的保证 介于 1 和 100 之间。我们可以编写一个测试,确保尝试创建一个 如果 Guess 实例的值超出该范围,则会出现 panic。


我们通过将 should_panic 属性添加到我们的 test 函数来实现这一点。如果函数内部的代码出现 panic,则测试通过;如果函数内部的代码没有 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` profile [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 时会 panic 的条件:

pub struct Guess {
    value: i32,
}

// --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 }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}


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

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [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`


在这种情况下,我们没有得到一个非常有用的消息,但是当我们查看 test 函数时,我们看到它用 #[should_panic] 进行了注释。我们得到的失败意味着 test 函数中的代码没有引起 panic。


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


文件名: src/lib.rs
pub struct Guess {
    value: i32,
}

// --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!,其中包含包含指定子字符串的 panic 消息


这个测试会通过,因为我们在 should_panic 属性的 expected 参数是 Guess::new function panics with 一起。我们可以指定整个 panic 消息,即我们 expect,在本例中为 Guess value must be less than or equal to 100, got 200 。您选择指定的内容取决于 panic 消息中有多少是唯一的或动态的,以及您希望测试的精确程度。在这种情况下,panic 消息的子字符串足以确保测试函数中的代码执行 else if value > 100 大小写。


查看当 should_panic 测试显示预期消息时会发生什么情况 失败,让我们通过交换 if 值 < 1else if 值 > 100 阻止:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        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}."
            );
        }

        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);
    }
}


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

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [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 src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
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`


失败消息表明此测试确实如我们预期的那样出现 panic,但 panic 消息不包含小于或等于 100 的预期字符串。在这种情况下,我们确实收到了 panic 消息,现在 Guess value must be greater than or equal to 1, got 200. 我们可以开始找出我们的 bug 在哪里了!


在测试中使用 result<T、E>


到目前为止,我们的测试在失败时都会感到恐慌。我们还可以编写使用 结果<T,E>!这是示例 11-1 中的测试,重写为使用 Result<T、E> 并返回一个 Err 而不是 panicing:

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() -> Result<(), String> {
        let result = add(2, 2);

        if result == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}


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


编写测试以使其返回 Result<T, E> 使您能够在测试正文中使用问号运算符,这可能是编写测试的便捷方法,如果测试中的任何作返回 Err 变体,则测试将失败。


您不能在使用 Result<T, E> 的测试上使用 #[should_panic] 注释。要断言作返回 Err 变体,请不要Result<T, E> 值使用问号运算符。相反,请使用 断言!(value.is_err())) 中。


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