13.3.测试的组织方式
测试是一套复杂的规则不同的人使用不同的术语和组织架构。Rust社区认为测试主要分为两类单元测试和集成测试。单元测试主要测试规模小更加关注功能一次只能测试一个模块而且能够测试私有接口。集成测试对于你的库来说是从外部进行测试并使用代码的方式完全相同于其他外部代码一样只使用公共接口且可能要跨多个模块。写这两种方式的测试都是重要的用于确保你的库符合你的预期无论是单独的还是一起进行测试。13.3.1 单元测试单元测试的目的是测试每一个代码块并精准定位该代码是否符合预期。你将单元测试放入src目录中的每一个文件需要进行测试的代码中。惯例是在每一个文件中创建一个名字叫tests的模块并使用cfg[test]进行声明其内可以包含测试函数。13.3.1.1 tests测试模块和#[cfg(test)]在tests模块之上的#[cfg(test)]声明会告诉Rust在运行cargo test命令这时需要编译和运行测试的代码而使用cargo bulid则不会编译和运行这些代码。这样就会节省大量的时间和空间因为没有包含测试代码。集成测试在不同的目录它不需要#[cfg(test)]声明但是因为单元测试和代码在同一个文件你必须使用#[cfg(test)]来告诉编译器不要将其编译进编译后的结果。再次回到生成的第一个库文件时为我们生成的测试代码pub fn add(left: u64, right: u64) - u64 { left right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result add(2, 2); assert_eq!(result, 4); } }在自动生成的tests模块属性cfg代表配置并且告诉Rust之后的测试项被包含宅配置选项中。在这种情况下配置项是test它会由Rust告诉编译器测试时进行编译。通过使用cfg属性运行cargo test命令只编译我们的测试代码。除了包含声明了#[test]的函数在这个模块中还会包含更有用的一些代码。13.3.1.2私有功能测试私有功能是否应该直接进行测试还有争论而其他编程语言很难或不可能测试私有功能。无论你坚持哪一种理念Rust允许你测试私有功能。下面的代码演示了如何测试私有功能internal_adderpub fn add_two(a: u64) - u64 { internal_adder(a, 2) } fn internal_adder(left: u64, right: u64) - u64 { left right } #[cfg(test)] mod tests { use super::*; #[test] fn internal() { let result internal_adder(2, 2); assert_eq!(result, 4); } }注意internal_adder函数没有标注为pub。子模块中的测试项能够使用祖先模块的中的成员项。在这个测试中在tests模块中使用use super::*我们将其所有的成员项带入tests模块然后就可以调用internal_adder函数了。13.3.2集成测试在Rust中集成测试对于你的库来说完全是外部的。它们使用你的库和其他代码相同这意味着你只能调用库中声明为pub的API。它们的目的是测试的库中的各个部分能够协调一致正常工作。在单元测试中能够正确的工作但是集成在一起不一定会没有问题因此整合在一起的代码完全测试也非常重要。为了创建集成测试你需要创建测试目录。测试目录我们创建tests目录在项目目录的根目录中与src毗邻。Cargo知道在这个目录中找到集成测试。我们接着能够在这个目录放入你想要的测试程序文件Cargo会将这些文件编译为单独的箱crate。让我们创建一个集成测试。首先创建tests目录然后再该目录中创建一个integration_test.rs文件这个项目目录结构如下所示adder ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests └── integration_test.rs在interation_test.rs文件中写入如下的代码use lession13_013::add; #[test] fn it_adds_two(){ let result add(2,2); assert_eq!(result,4); }在tests目录的每一个文件都是一个独立的箱因此我们需要将需要测试的库带入到每一个测试箱中的作用域。为了这种测试我们增加lession13_013::add在代码的顶部我们不需要单元测试。我们不需要在integration_test.rs文件中声明#[cfg(test)]。cargo会特殊对待tests目录并且在使用cargo test命令是会编译这个目录中的文件。下面是运行该命令后的输出信息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 Running tests\integration_test.rs (target\debug\deps\integration_test-2c4bbd8eb468028f.exe) running 1 test test it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests lession13_013 running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s输出信息包含三段信息单元测试、集成测试和文档测试。注意任何测试类别失败后面的测试都不会运行。例如如果单元测试失败则集成测试和文档测试都不会有任何信息输出只有在单元测试通过后面的测试才会执行。第一段是单元测试每个单元测试占据一行最后一行是总述。第二段是集成测试列出来文件名和编译后的文件名。其后一行列出集成测试的测试项各占据一行最后是集成测试的总述。每个集成测试都有自己的一段因此在tests目录中可以增加多个文件输出结果也会显示多个集成测试段。第三段是文档测试的内容格式和之前的单元测试一样。我们也可以指定继承测试的名称来单独执行继承测试。为了对某个继承测试单独执行其内的所有测试项可以使用--test参数之后加上文件名。如下所示cargo test --test integration_test warning: C:\Users\xxx\.cargo\config is deprecated in favor of config.toml | help: if you need to support cargo 1.38 or earlier, you can symlink config to config.toml Finished test profile [unoptimized debuginfo] target(s) in 0.08s Running tests\integration_test.rs (target\debug\deps\integration_test-2c4bbd8eb468028f.exe) running 1 test test it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s指定集成测试也支持文件名的通配符例如cargo test --test integration*集成测试中的子模块当你增加了更多的集成测试你需要在tests目录中增加更多的文件来组织这些测试例如你可以将测试按照功能进行分组每一个文件被编译为一个独立的箱中对于创建隔离的作用域是非常有用的以便终端用户可以更好的模拟真实环境那样使用这些箱。但是这些在tests目录中的文件不会像src目录中文件那样会共享相同的行为。在多个集成测试文件中有一组都会使用的辅助功能如果要将其抽取放入一个通用的模块中例如放入到tests/common.rs文件中在其内写一个setup函数以便在多个集成测试中调用它pub fn setup() { // setup code specific to your librarys tests would go here }当运行测试可以看到信息输出中有common.rs文件即使它没有包含任何测试代码也没有调用setup函数它也会出现在测试输出的信息中。Running tests\common.rs (target\debug\deps\common-6fd8f73cd685061c.exe) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s如果我们不想要显示这些内容只想显示需要的内容可以创建common目录其内创建mod.rs文件。整个项目的目录如下所示├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests ├── common │ └── mod.rs └── integration_test.rs这样Rust就不会单独编译mod.rs文件测试结果信息也不会包含不需要的内容。将之前common文件的内容复制到mod.rs文件中并修改integration_test文件中的内容如下所示use lession13_013::add; mod common; #[test] fn it_adds_two(){ common::setup(); let result add(2,2); assert_eq!(result,4); }注意mod common的引入和src中的模块引入一样。然后我们就可以在测试函数中调用common::setup()这个函数了。为集成测试创建二进制文件如果我们的项目只包含src中main.rs文件并不包含src/lib.rs文件我们就不能在测试目录中创建集成测试而且也不能使用use语句引入main.rs文件中定义的函数。只有库箱才可以像其他箱暴露可以使用的功能。二进制文件的初衷就意味着独立运作。这是Rust项目提供一个简洁的main.rs文件的原因之一该main.rs文件可以调用保存在lib.rs文件中的逻辑。使用这种结构可以使集成测试调用库箱从而可以使用其内的重要功能。如果重要功能起作用则main文件即使少量的代码也可以很好的工作也就意味着测试只需更少的代码。13.4总结Rust测试特性提供了一种方法来规范代码如何组织才能保证如期所愿即使修改了代码测试也可以达到效果。单元测试遍布整个库的不同部分并且可以测试私有实现的细节。集成测试用于验证库代码各个不同部分的协调工作的正确性。一般调用库提供的公共API并如同外部代码那样调用。即使Rust的类型系统和所有权规则阻止了一些漏洞测试对于减少逻辑错误并确保程序如期动作还是非常重要的。