十九、测试
Published on
测试是检查其它代码是否正常工作的Rust函数。典型的测试函数执行以下三个动作:
- 设置需要的数据和状态
 - 运行测试代码
 - 检查结果是否符合预期
 
如何编写测试函数
使用cargo new建立一个库项目时,会自动生成一个测试模块。测试函数前需要添加#[test]属性,之后使用cargo test命令会自动运行测试。
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}
使用assert!宏检查结果
assert!用于保证条件为true,为true则继续执行程序,为false则调用panic!。
#[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));
    }
    // 此测试会失败
    #[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));
    }
}
使用assert_eq!与assert_ne!宏检查相等
// 函数无bug,测试通过
pub fn add_two(a: i32) -> i32 {
    a + 2
}
// 函数有bug,测试不通过
pub fn add_two(a: i32) -> i32 {
    a + 3
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }
}
添加自定义失败信息
使用assert!等宏时可以添加额外参数用于打印自定义失败信息,额外参数被传入format!宏进行格式化。
assert!(
    result.contains("Carol"),
    "Greeting did not contain name, value was `{}`",
    result
);
使用should_panic检查panic
在测试函数前添加#[should_panic]属性,此时如果此函数panic则测试通过,不panic则测试不通过。
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}
使用should_panic的expected参数指定panic中必须包含指定字符串:
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}
在测试中使用Result
测试函数可以返回Result,如果有错误则返回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"))
        }
    }
}
测试返回Result时不能使用#[should_panic]属性,如果需要判断一个操作是否返回Err,不要使用?操作符,使用assert!(value.is_err())判断。
控制测试如何运行
并行或顺序运行测试
当运行测试时,默认使用多线程,此时必须保证测试之间不能相互信赖。如果要顺序执行,可以使用–test-threads参数:
$ cargo test -- --test-threads=1
显示函数输出
默认情况下,被测试代码的输出不会显示,如下面的函数中的println!宏的输出:
fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {}", a);
    10
}
如果需要显示,使用– –show-output参数:
$ cargo test -- --show-output
使用名称指定运行部分测试
运行单个测试:
cargo test one_hundred
运行所有包含add的测试
cargo test add
忽略一些测试
在测试函数前添加#[ignore]属性,则默认情况下此测试被忽略。
#[test]
#[ignore]
fn expensive_test() {
}
如果要只运行这些测试则使用:
cargo test -- --ignored
如果要运行全部测试则使用:
cargo test -- --include-ignored
单元测试
单元测试的目的是独立地测试每个单元的功能,代码放在每个源文件内部,惯例是添加一个名为tests的模块并添加#[cfg(test)]属性。
tests模块与#[cfg(test)]属性
由于单元测试在代码文件中,一般使用#[cfg(test)]属性表示不要在编译时包含测试代码。
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}
测试私有函数
由于Rust的隐私规则,在子模块中可以测试父模块的私有函数。
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));
    }
}
集成测试
集成测试在库的外部,与其它代码使用同样的方式调用库,因此只能调用公有接口。集成测试用于测试库中不同部分之间在一起可以正常工作。
tests目录
在src目录同级下建立tests目录,Cargo会在该目录下查找集成测试文件。
adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs
tests目录中的集成测试文件需要使用use导入需要的库,并且不需要使用#[cfg(test)]属性。
use adder;
#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}
cargo test命令可以使用–test参数运行指定集成测试:
cargo test --test integration_test
集成测试中的子模块
与src目录不同,tests目录中的每个文件被编译为独立的箱,因此更加独立。假设tests/common.rs文件中有一个公用函数:
pub fn setup() {
}
可以在tests/integration_test.rs中使用该函数:
use adder;
// 声明common模块
mod common;
#[test]
fn it_adds_two() {
    // 调用common模块的代码
    common::setup();
    assert_eq!(4, adder::add_two(2));
}
tests中每个文件都会进行测试并输出log,如果不想测试,可以将文件放入文件夹中,如tests/common/mod.rs。
对二进制箱进行集成测试
tests目录中的代码无法对二进制箱进行测试,因为无法使用use进行导入。因此Rust提供一个直接调用src/lib.rs文件的src/main.rs文件,src/lib.rs文件可以进行集成测试而src/main.rs文件中的少量代码无需测试。
文档测试
Rust中使用///添加文档注释,文档注释为Markdown格式,其中可以添加代码块,这些代码块被自动编译为文档测试。
/// First line is a short summary describing function.
///
/// The next lines present detailed documentation. Code blocks start with
/// triple backquotes and have implicit `fn main()` inside
/// and `extern crate <cratename>`. Assume we're testing `doccomments` crate:
///
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
开发信赖
有时只有在测试时需要添加一些信赖,这些信赖添加至Cargo.toml的[dev-dependencies]节中。
[dev-dependencies]
pretty_assertions = "1"