Wang's blog

十九、测试

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"