如何编写测试

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

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

让我们看看 Rust 专门为编写测试提供的功能,这些测试 执行这些作,包括 attribute、一些宏和 attribute。testshould_panic

测试函数剖析

简单来说,Rust 中的测试是一个用 attribute 注解的函数。属性是关于 Rust 代码片段的元数据;一个例子是 我们在第 5 章中对 structs 使用的属性。更改函数 添加到 test 函数中,在 .当您运行 tests 的 Rust 会构建一个测试运行程序二进制文件,该二进制文件运行 带注释的函数,并报告每个测试函数是否通过 或 失败。testderive#[test]fncargo test

每当我们使用 Cargo 创建一个新的库项目时,一个带有 test 函数是自动生成的。此模块为您提供一个 模板来编写测试,因此您不必查找确切的 结构和语法。您可以添加任意数量的 额外的测试功能和任意数量的测试模块!

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

让我们创建一个名为 that will add two numbers 的新 library 项目:adder

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

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

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

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

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

该命令运行我们项目中的所有测试,如 清单 所示 11-2.cargo test

$ 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 编译并运行了测试。我们看到 .下一个 line 显示生成的测试函数的名称,名为 , 并且运行该测试的结果是 。总体摘要表示所有测试都已通过,读取的部分表示通过或失败的测试总数。running 1 testtests::it_worksoktest result: ok.1 passed; 0 failed

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

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

我们可以向命令传递一个参数,以仅运行其 name 匹配字符串;这称为筛选,我们将在 “按名称运行测试子集” 一节中介绍。在这里,我们 尚未筛选正在运行的测试,因此摘要的末尾显示 .cargo test0 filtered out

测试输出的下一部分从 开始,用于 任何文档测试的结果。我们还没有任何文档测试, 但 Rust 可以编译出现在我们 API 文档中的任何代码示例。 此功能有助于使您的文档和代码保持同步!我们将讨论如何 在 “Documentation Comments as (文档注释) 中编写文档测试 Tests“部分。现在,我们将 忽略 output。Doc-tests adderDoc-tests

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

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

现在我们将添加另一个测试,但这次我们将进行一个失败的测试!测试 fail 当 test 函数中的某些内容出现 panic 时。每个测试都在一个新的 线程,当主线程看到测试线程已死亡时,测试为 标记为失败。在第 9 章中,我们讨论了如何消除 panic 的最简单方法 是调用宏。将新测试作为名为 的函数输入,因此您的 src/lib.rs 文件类似于示例 11-3。panic!another

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

使用 再次运行测试。输出应类似于 Listing 11-4,这表明我们的测试通过了,但失败了。cargo testexplorationanother

$ 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:一个测试通过和一个测试失败时的测试结果

该行显示 ,而不是 。两个新 各个部分出现在各个结果和摘要之间:第一个 显示每个测试失败的详细原因。在这种情况下,我们得到 由于 it 打开 而失败的详细信息 src/lib.rs 文件中的第 17 行。下一部分仅列出所有 失败的测试,这在存在大量测试和大量 详细的失败测试输出。我们可以使用失败测试的名称来运行 that test 以更轻松地调试它;我们将更多地讨论在 “控制测试的运行方式”部分。oktest tests::anotherFAILEDanotherpanicked at 'Make this test fail'

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

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

使用 assert!

标准库提供的宏在您需要时非常有用 以确保测试中的某些条件的计算结果为 。我们给宏一个计算结果为 Boolean 的参数。如果值为 ,则不会发生任何情况,并且测试通过。如果值为 ,则宏将调用导致测试失败。使用宏可以帮助我们检查代码是否按预期方式运行。assert!trueassert!truefalseassert!panic!assert!

在第 5 章示例 5-15 中,我们使用了一个 struct 和一个方法,在示例 11-5 中重复了这两个步骤。让我们把这段代码放在 src/lib.rs 文件中,然后使用宏为它编写一些测试。Rectanglecan_holdassert!

文件名: 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 章中的结构体及其方法Rectanglecan_hold

该方法返回一个 Boolean,这意味着它是一个完美的用例 对于宏。在示例 11-6 中,我们编写了一个测试,通过创建一个宽度为 8 的实例来执行该方法,并且 height 为 7 并断言它可以容纳另一个实例 宽度为 5,高度为 1。can_holdassert!can_holdRectangleRectangle

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

请注意模块内的行。该模块是 一个常规模块,遵循我们在 章节 中介绍的通常可见性规则 7 在“引用模块中项目的路径” Tree“部分。因为 module 是一个内部 module,所以我们需要将 外部模块中的待测试代码添加到内部模块的范围内。我们使用 一个 glob,所以我们在 outer module 中定义的任何东西都可用于这个模块。use super::*;teststeststeststests

我们已经将 test 命名为 ,并创建了所需的两个实例。然后我们调用宏 和 将调用 .此表达式为 应该返回 ,因此我们的测试应该通过。让我们来了解一下!larger_can_hold_smallerRectangleassert!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

它确实通过了!让我们添加另一个测试,这次断言一个较小的 rectangle 无法容纳更大的 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() {
        // --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));
    }
}

因为在这种情况下,函数的正确结果是 , 在将该结果传递给宏之前,我们需要将其否定。作为 result,如果返回 :can_holdfalseassert!can_holdfalse

$ 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`

我们的测试发现了错误!因为 is 和 is ,所以 中的宽度比较现在返回 : 8 不是 小于 5。larger.width8smaller.width5can_holdfalse

使用 assert_eqassert_ne!

验证功能的一种常见方法是测试结果之间的相等性 )和您希望代码返回的值。你可以 通过使用宏并使用运算符向其传递表达式来执行此作。但是,这是一个非常常见的测试,标准库 提供一对宏 — 和 — 来执行此测试 更方便。这些宏比较两个参数是否相等或 不等式。如果断言 fails 的 Fails,这样更容易看到测试失败的原因;相反,宏仅指示它获取了表达式的值,而不打印导致该值的值。assert!==assert_eq!assert_ne!assert!false==false

在示例 11-7 中,我们编写了一个名为 的函数,该函数将其添加到其 parameter 参数,那么我们使用宏来测试这个函数。add_two2assert_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:使用宏测试函数add_twoassert_eq!

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

$ 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

我们创建一个名为 的变量,该变量保存调用 .然后我们将 and 作为参数传递给 。 此测试的输出行为 ,文本指示我们的测试通过!resultadd_two(2)result4assert_eq!test tests::it_adds_two ... okok

让我们在代码中引入一个 bug,看看当它 失败。更改函数的实现以改为添加 :assert_eq!add_two3

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_twoassertion `left == right` failedleftrightleftadd_two(2)5right4

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

如果我们给它的两个值不相等,那么宏就会通过, 如果它们相等,则失败。这个宏在我们不确定的情况下最有用 值会是什么,但我们知道值绝对不应该是什么。 例如,如果我们正在测试一个保证会更改其输入的函数 在某种程度上,但 input 的更改方式取决于 在我们运行测试的那一周,最好断言的可能是 函数的 output 不等于 input。assert_ne!

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

添加自定义失败消息

您还可以添加自定义消息,以将失败消息打印为 、 和 宏的可选参数。任何 在将所需参数传递给宏之后指定的参数(在第 8 章的“使用 + 运算符或格式连接!Macro“ 部分),以便您可以传递包含占位符和 值以放入这些占位符中。自定义消息可用于记录 断言的含义;当测试失败时,您将更好地了解 问题出在代码上。assert!assert_eq!assert_ne!format!{}

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

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

此计划的要求尚未达成一致,我们正在 很确定问候语开头的文本会发生变化。我们 决定我们不想在需求发生变化时更新测试, 因此,我们无需检查与函数返回的值是否完全相等,而是直接断言输出包含 input 参数。Hellogreeting

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

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`

此结果仅指示断言失败,以及 断言已打开。更有用的失败消息将打印函数中的值。让我们添加一条由格式 string 中,占位符填充了我们从函数获得的实际值:greetinggreeting

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 检查 Panics

除了检查返回值之外,检查我们的代码是否 按照我们的预期处理错误条件。例如,考虑 type 我们在第 9 章示例 9-13 中创建的。使用的其他代码取决于实例仅包含值的保证 介于 1 和 100 之间。我们可以编写一个测试,确保尝试创建值超出该范围的实例时会出现 panic。GuessGuessGuessGuess

我们通过将 attribute 添加到我们的 test 函数来实现这一点。这 如果函数内部的代码 panic,则测试通过;如果代码 内部的函数不会 panic。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!

我们将 attribute 放在 attribute 之后,然后 在它适用的 test 函数之前。让我们看看这个测试时的结果 通过:#[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:new

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 函数中的代码没有导致 panic。#[should_panic]

使用的测试可能不精确。测试将 通过,即使测试因与我们不同的原因而 panic 期待。为了使测试更精确,我们可以向 attribute 添加一个可选参数。测试框架将 确保 failure 消息包含提供的文本。例如 考虑示例 11-9 中修改后的代码,其中函数 panics 并显示不同的消息,具体取决于值是否太小或 太大了。should_panicshould_panicshould_panicexpectedshould_panicGuessnew

文件名: 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 消息的 apanic!

这个测试会通过,因为我们在 attribute 的 parameter 中输入的值是函数 panic 的 message 的子字符串。我们可以指定整个 panic 消息,即我们 expect,在本例中为 。您选择指定的内容取决于 panic 的程度 消息是唯一的或动态的,以及您希望测试的精确程度。在这个 case,则 panic 消息的子字符串足以确保 test 函数执行 case。should_panicexpectedGuess::newGuess value must be less than or equal to 100, got 200else if value > 100

查看当测试中带有消息 失败后,让我们通过交换 the 和 the blocks 的主体,再次在我们的代码中引入一个 bug:should_panicexpectedif value < 1else if value > 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 消息不包含预期的字符串 .在这种情况下,我们确实收到了 panic 消息,现在我们可以开始弄清楚哪里了 我们的 bug 是!less than or equal to 100Guess value must be greater than or equal to 1, got 200.

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

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

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

该函数现在具有 return 类型。在 body 的函数,而不是调用宏,我们在测试通过时返回,并在测试 失败。it_worksResult<(), String>assert_eq!Ok(())ErrString

编写测试以使其返回 a 使您能够使用问题 mark 运算符,这是一种方便的编写方式 如果其中的任何作返回 variant,则测试应该失败。Result<T, E>Err

您不能在使用 .要断言作返回变体,请不要使用 问号运算符。请改用 .#[should_panic]Result<T, E>ErrResult<T, E>assert!(value.is_err())

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

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准