15.编写自动化测试

本文详细介绍了Rust编程中如何编写和管理测试,包括单元测试、集成测试和文档测试。通过示例展示了如何使用assert_eq!、assert_ne!宏进行断言,如何使用should_panic属性测试 panic,以及如何组织测试代码。此外,还讨论了测试的并行运行、控制输出和过滤测试等高级话题,强调了测试在确保代码质量中的重要性。
摘要由CSDN通过智能技术生成

编写自动化测试

“Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.”这不意味着需要尽可能的测试软件

  • 例如,我们可以编写一个叫做 add_two 的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的类型检查和借用检查,例如,这些检查会确保我们不会传递 String 或无效的引用给这个函数。Rust 所 不能 检查的是这个函数是否会准确的完成我们期望的工作:返回参数加 2 后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。

如何编写测试

  • Rust 中的测试函数是用来验证非测试代码是否按照期望的方式运行的。测试函数体通常执行如下三种操作:
    • 设置任何所需的数据或状态
    • 运行需要测试的代码
    • 断言其结果是我们所期望的
  • Rust中有专门的用于编写测试的功能:test属性、一些宏和should_panic属性

测试函数剖析

  • 作为简单的例子,Rust中测试是一个带有test属性标注的函数。属性(attribute)是关于Rust代码片段的元数据。derive就是一个属性(#[derive(Debug)]),如果要进行一个测试函数,需要在fn行之前加上#[test],当使用cargo test时,构建一个测试执行程序用来标记test属性的函数,并且报告每一个测试是否通过。
  • 第7章当使用Cargo新建项目库,会自动生成一个测试模块和测试函数。
  • 创建新的库项目adder:
cargo new adder --lib
cd adder
fn main() {}
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

  • #[test]:在函数之前的这个参数表明的是这个函数是一个测试函数。
  • 函数体通过assert_eq!宏断言2+2 = 4。
Compiling adder v0.1.0 (E:\RUST\adder)
    Finished test [unoptimized + debuginfo] target(s) in 1.42s
     Running unittests src\lib.rs (target\debug\deps\adder-b7a94d0e2e80adf5.exe)

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    
  • 这段测试是可以正常运行的,test result也证实了这一点。因为没有将任何测试标记为忽略,所以显示0 ignored,也没有过滤要运行的测试,所以显示 0 filtered out
  • 0 measured 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。
  • Doc-tests adder 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。
fn main() {}
#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

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

  • test tests::another ... failed表明了another测试失败。
running 2 tests
test tests::exploration ... ok
test tests::another ... FAILED

failures:

---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.

failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed

使用assert!宏检查结果

  • assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。需要向 assert! 宏提供一个求值为布尔值的参数。如果值是 trueassert! 什么也不做,同时测试会通过。如果值为 falseassert! 调用 panic! 宏,这会导致测试失败。assert! 宏帮助我们检查代码是否以期望的方式运行。
  • Rectangle结构体和can_hold方法进行修改,使得它们进行测试。
fn main() {}
#[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
    }
}

  • can_hold返回一个布尔值,它完美符合assert!的使用场景,编写一个测试进行练习,创建一个长是8宽是7的Rectangle实例,并假设它能放得下另一个长度为5宽为1的Rectangle实例
  • use super::*;的方法是将相对路径中的所有项都引入到内部模块的作用域之中,"*`"的意思是全局导入,这种方法虽然能够将所有函数导入,但是得注意阅读的方便性问题。
  • 接着,放入需要测试的代码:
#[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));
    }
}

  • 这样是可以正常测试的。
  • 修改代码,断言小的矩形放不下大的矩形。
#[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 smaller_cannot_hold_larger() {
        let larger = Rectangle { width: 8, height: 7 };
        let smaller = Rectangle { width: 5, height: 1 };

        assert!(!smaller.can_hold(&larger));
    }
}
  • 这段代码也是可以正常测试的。但是这里要注意前面有取反符号!,用来将smaller.can_hold(&larger)结果取反,取反之后为true,传递给assert!宏。
  • 对代码修改,会传递错误,官方文档
    在这里插入图片描述

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

  • 通过使用==运算符的表达式来做到。这两个宏分别比较两个值相等还是不相等。断言失败的时候会打印出来两个值的具体的含义,用来观察为什么失败,这相较于assert!宏较为高级,而assert!宏只会打印出错误,并不会打印出导致错误的原因。
fn main() {}
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));
    }
}

  • 测试可以正常通过。
  • 如果添加一个bug,将a+2改成a+3,
  • 测试会出Bug,原因会列出来:
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` for a backtrace.

failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

  • 相对应的是,assert_ne!会在值不相等的情况下输出。
  • 当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 PartialEqDebug trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 PartialEq 才能断言他们的值是否相等。需要实现 Debug 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第 5 章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 #[derive(PartialEq, Debug)] 标注。附录 C “可派生 trait” 中有更多关于这些和其他派生 trait 的详细信息。

自定义失败信息

  • 你也可以向 assert!assert_eq!assert_ne! 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 assert! 的一个必需参数和 assert_eq!assert_ne! 的两个必需参数之后指定的参数都会传递给 format! 宏,所以可以传递一个包含 {} 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。
fn main() {}
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"));
    }
}

  • contains函数中是某个字符串是否包含另外一个字符串。但是此代码测试是可以通过的,将代码修改一下,使其出现bug:
fn main() {}
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"));
    }
}

  • 这样就会产生错误,
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` for a backtrace.

failures:
    tests::greeting_contains_name

  • 但是它仅仅产生了失败的行号和信息,让我们自己添加一个错误失败的信息参数:带占位符格式字符串,和greeting函数的值,
#[test]
fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
        result.contains("Carol"),
        "Greeting did not contain name, value was `{}`", result
    );
}

  • 这样就可以看出确切的值,会帮助我们了解发生了什么,而不是期望值。

使用should_panic检查panic

  • 写一个函数确保Guess实例中的值在1到100之间,超过100的时候会出现panic
  • 对函数增加一个should_panic实现这些,这个属性在函数中代码panic!的时候会通过,没有panic的时候失败。也就是说,程序员在这个点预判接下来程序会panic
fn main() {}
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);
    }
}

  • 这种情况之下,如果程序原本可以正常执行,他就会出现panic,如果程序原来出现panic,他就会正常测试,在这段代码中由于200>100,所以会正常执行,
running 1 test
test tests::greater_than_100 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

  • 接着是一种变形:
fn main() {}
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
        }
    }
}

running 1 test
test tests::greater_than_100 ... FAILED

failures:

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

  • 之前的should_panic只是会出现,如果想要should_panic测试结果精确表述,使用expected参数提供值是Guess::new函数panic的子串,在这个例子中是 Guess value must be less than or equal to 100, got 200. 。 expected 信息的选择取决于 panic 信息有多独特或动态,和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在 else if value > 100 的情况下运行。
fn main() {}
pub struct Guess {
    value: i32,
}
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 = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

  • 将value<1和value>100的代码进行调换,
fn main() {}
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 = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

  • 现在运行,结果会出现失败,
running 1 test
test tests::greater_than_100 ... 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:11:13
note: Run with `RUST_BACKTRACE=1` for a backtrace.
note: Panic did not include expected string 'Guess value must be 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

  • 失败信息表明测试确实如期望 panic 了,不过 panic 信息中并没有包含 expected 信息 'Guess value must be less than or equal to 100'。而我们得到的 panic 信息是 'Guess value must be greater than or equal to 1, got 200.'。这样就可以开始寻找 bug 在哪了!
  • 另外一个要说明的点是:panic存在是因为should panic中子串和Guess::new中的字符串不相同。

将Result<T,E>用于测试

  • 使用panic,就也可以使用Result:
fn main() {
#[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"))
        }
    }
}
}
  • 这样编写测试,在正确执行不调用assert_eq!宏而是返回Ok(()).
  • 这样编写同时可以使用?运算符简化,但是不能再对它进行#[should_panic]标注,测试失败直接返回Err

控制测试如何运行

  • 可以将一部分命令行参数传递给cargo test,另外一部分传递给生成的测试二进制文件,为了分割这两种参数,需要先列出传递给cargo test的参数,接着是分隔符–,再之后是传递给测试二进制文件的参数。运行cargo test --help 会提示cargo test的有关参数,而运行cargo test -- --help可以提示在分隔符--之后使用的有关参数。

并行或者连续的运行测试

  • 当运行多个测试时, Rust 默认使用线程来并行运行。这意味着测试会更快地运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。
  • 举个例子,每一个测试都运行一些代码,假设这些代码都在硬盘上创建一个 test-output.txt 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中修改了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干扰。一个解决方案是使每一个测试读写不同的文件;另一个解决方案是一次运行一个测试。
  • 如果不希望测试并行运行,或者需要更加精准的控制线程的数量,将--test-threads参数和希望使用线程的数量给测试二进制文件。如:
cargo test -- --test-thread=1
  • 将测试线程设置为1.告诉程序不要使用任何并行机制,这也会比并行运行花费更多时间,不过有共享的状态,测试就不会存在潜在的相互干扰了。

显示函数输出

  • 默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容。比如在测试中调用了 println! 而测试通过了,我们将不会在终端看到 println! 的输出:只会看到说明测试通过的提示行。如果测试失败了,则会看到所有标准输出和其他错误信息。
  • 示例有一个无意义的函数,打印出其参数的值并且接着返回10,接着是一个通过的测试和失败的测试。
fn main() {
fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {}", a);
    10
}

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

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(10, value);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(5, value);
    }
}
}
  • 运行cargo test会看到这些测试的输出
running 2 tests
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.

failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

  • 注意输出中不会出现测试通过时打印的内容,即 I got the value 4。因为当测试通过时,这些输出会被截获。失败测试的输出I got the value 8 ,则出现在输出的测试摘要部分,同时也显示了测试失败的原因。
  • 如果想要看通过的测试中打印的值,截获输出的行为通过--nocapture参数来禁用:
cargo test -- --nocapture
  • 再次运行,得到:
running 2 tests
I got the value 4
I got the value 8
test tests::this_test_will_pass ... ok
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.
test tests::this_test_will_fail ... FAILED

failures:

failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

通过指定的名字来运行部分测试

  • 有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行与这些代码相关的测试。你可以向 cargo test 传递所希望运行的测试名称的参数来选择运行哪些测试。
  • 为展示如何运行部分测试,示例添加了三个测试,选择具体运行哪一个:
fn main() {
pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

    #[test]
    fn add_two_and_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn add_three_and_two() {
        assert_eq!(5, add_two(3));
    }

    #[test]
    fn one_hundred() {
        assert_eq!(102, add_two(100));
    }
}
}
  • 如果直接运行测试,所有的测试都会并行运行:
running 3 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok
test tests::one_hundred ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

运行单个测试

cargo test one_hundred
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running target/debug/deps/adder-06a75b4a1f2515e9

running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
  • 只有名称为 one_hundred 的测试被运行了;因为其余两个测试并不匹配这个名称。测试输出在摘要行的结尾显示了 2 filtered out 表明还存在比本次所运行的测试更多的测试被过滤掉了。

  • 不能像这样指定多个测试名称;只有传递给 cargo test 的第一个值才会被使用。不过有运行多个测试的方法。

过滤运行多个测试

  • 我们可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行。例如,因为头两个测试的名称包含 add,可以通过 cargo test add 来运行这两个测试:
cargo test add
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running target/debug/deps/adder-06a75b4a1f2515e9

running 2 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out

忽略某些测试

  • 有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 cargo test 的时候希望能排除他们。虽然可以通过参数列举出所有希望运行的测试来做到,也可以使用 ignore 属性来标记耗时的测试并排除他们,如下所示:
fn main() {
#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
    // 需要运行一个小时的代码
}
}
  • 对于想要排除的测试,我们在 #[test] 之后增加了 #[ignore] 行。现在如果运行测试,就会发现 it_works 运行了,而 expensive_test 没有运行:
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.24 secs
     Running target/debug/deps/adder-ce99bcc2479f4607

running 2 tests
test expensive_test ... ignored
test it_works ... ok

test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out

  • 如果只是希望运行被忽略的测试,可以使用cargo test -- --ignored
$ cargo test -- --ignored
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running target/debug/deps/adder-ce99bcc2479f4607

running 1 test
test expensive_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out

测试的组织结构

  • Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与 集成测试(integration tests)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。

单元测试

  • 单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的某个单元的代码功能是否符合预期。单元测试与他们要测试的代码共同存放在位于 src 目录下相同的文件中。规范是在每个文件中创建包含测试函数的 tests 模块,并使用 cfg(test) 标注模块。

测试模块和#[cfg(test)]

  • 测试模块的#[cfg(test)]标注告诉Rust只在执行 cargo test 时才编译和运行测试代码,而在运行 cargo build 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 #[cfg(test)] 标注。然而单元测试位于与源码相同的文件中,所以你需要使用 #[cfg(test)] 来指定他们不应该被包含进编译结果中。
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
}
  • 上述代码就是自动生成的测试模块。cfg 属性代表 configuration ,它告诉 Rust 其之后的项只应该被包含进特定配置选项中。在这个例子中,配置选项是 test,即 Rust 所提供的用于编译和运行测试的配置选项。通过使用 cfg 属性,Cargo 只会在我们主动使用 cargo test 运行测试时才编译测试代码。这包括测试模块中可能存在的帮助函数, 以及标注为 #[test] 的函数。

测试私有函数

  • Rust中允许你进行私有函数的测试,考虑示例中私有函数测试的代码。
fn main() {}

pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

  • 注意 internal_adder 函数并没有标记为 pub,不过因为测试也不过是 Rust 代码同时 tests 也仅仅是另一个模块,我们完全可以在测试中导入和调用 internal_adder。如果你并不认为应该测试私有函数,Rust 也不会强迫你这么做。

集成测试

  • Rust 中,集成测试对于你需要测试的库来说完全是外部的。同其他使用库的代码一样使用库文件,也就是说它们只能调用一部分库中的公有 API 。集成测试的目的是测试库的多个部分能否一起正常工作。一些单独能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,你需要先创建一个 tests 目录。

tests目录

  • 为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。
  • 让我们创建一个集成测试,创建一个tests目录,和src同级,cargo会将每个文件当作单独的crate编译。
use adder;
#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}
  • 与单元测试不同,我们需要在文件顶部添加 use adder。这是因为每一个 tests 目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。
  • 并不需要将 tests/integration_test.rs 中的任何代码标注为 #[cfg(test)]tests 文件夹在 Cargo 中是一个特殊的文件夹, Cargo 只会在运行 cargo test 时编译这个目录中的文件。现在就运行 cargo test 试试:
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running target/debug/deps/adder-abcabcabc

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/integration_test-ce99bcc2479f4607

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

  • 我们已经知道,单元测试函数越多,单元测试部分的结果行就会越多。同样的,在集成文件中增加的测试函数越多,也会在对应的测试结果部分增加越多的结果行。每一个集成测试文件有对应的测试结果部分,所以如果在 tests 目录中增加更多文件,测试结果中就会有更多集成测试结果部分。
  • 我们仍然可以通过指定测试函数的名称作为 cargo test 的参数来运行特定集成测试。也可以使用 cargo test--test 后跟文件的名称来运行某个特定集成测试文件中的所有测试:
$ cargo test --test integration_test
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running target/debug/integration_test-952a27e0126bb565

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

  • 这个命令只运行了test中指定的文件integration_test.rs中的测试。

集成测试中的子模块

  • 随着集成测试的增加,你可能希望在 tests 目录增加更多文件以便更好的组织他们,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 tests 目录中的文件都被编译为单独的 crate
  • 将每个集成测试文件当作其自己的 crate 来对待,这更有助于创建单独的作用域,这种单独的作用域能提供更类似与最终使用者使用 crate 的环境。但是,tests 目录中的文件不能像 src 中的文件那样共享相同的行为。
  • 当你有一些在多个集成测试文件都会用到的帮助函数,而你尝试按照第 7 章 “将模块移动到其他文件” 部分的步骤将他们提取到一个通用的模块中时, tests 目录中不同文件的行为就会显得很明显。例如,如果我们可以创建 一个tests/common.rs 文件并创建一个名叫 setup 的函数,我们希望这个函数能被多个测试文件的测试函数调用:
pub fn setup() {
    // 编写特定库测试所需的代码
}
  • 同时,运行测试,会看到关于common.rs的文件测试结果部分。
running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/common-b8b07b6f1be2db70

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/integration_test-d993c68b431d39df

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

  • 我们并不想要common 出现在测试结果中显示 running 0 tests 。我们只是希望其能被其他多个集成测试文件中调用罢了。

  • 为了不让 common 出现在测试输出中,我们将创建 tests/common/mod.rs ,而不是创建 tests/common.rs 。这是一种 Rust 的命名规范,这样命名告诉 Rust 不要将 common 看作一个集成测试文件。将 setup 函数代码移动到 tests/common/mod.rs 并删除 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中。

  • 一旦拥有了 tests/common/mod.rs,就可以将其作为模块以便在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用 setup 函数的 it_adds_two 测试的例子:

use adder;

mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

二进制crate的集成测试

  • 如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 目录创建集成测试并使用 extern crate 导入 src/main.rs 中定义的函数。只有库 crate 才会向其他 crate 暴露了可供调用和使用的函数;二进制 crate 只意在单独运行。

  • 为什么 Rust 二进制项目的结构明确采用 src/main.rs 调用 src/lib.rs 中的逻辑的方式?因为通过这种结构,集成测试 就可以 通过 extern crate 测试库 crate 中的主要功能了,而如果这些重要的功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。

总结

  • Rust 的测试功能提供了一个确保即使你改变了函数的实现方式,也能继续以期望的方式运行的途径。单元测试独立地验证库的不同部分,也能够测试私有函数实现细节。集成测试则检查多个部分是否能结合起来正确地工作,并像其他外部代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码中不符合期望行为的逻辑 bug 仍然是很重要的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值