Skip to content

为什么要编写测试?

TDD 的开发方式好处很多, 如:

  • 修改代码后及时发现错误
  • 小步走, 快速验证
  • 确定被测试代码的输入输出, 解耦合

如何编写自动化测试

做个小案例:

  1. 利用 Vector 实现 Stack 数据结构, 编写单元测试, 验证 Stack 数据是否处理正确
  2. 利用 Stack 数据结构, 写一个十进制数转二进制字符串

新建项目

sh
cargo new unittests

目录结构

txt
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── lib.rs
    ├── linear
    │   ├── stack.rs
    └── main.rs

代码

重点是单元测试, 所以带有单元测试的代码放最前面

rust
#[derive(Debug)]
pub struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    pub fn new() -> Self {
        Self {
            items: Vec::new(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    pub fn size(&self) -> usize {
        self.items.len()
    }

    pub fn push(&mut self, item: T) {
        self.items.push(item);
    }

    pub fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    pub fn peek(&self) -> Option<&T> {
        self.items.last()
    }

    pub fn clear(&mut self) {
        self.items.clear();
    }
}

impl<T> Default for Stack<T> {
    fn default() -> Self {
        Self::new()
    }
}

// Stack 的单元测试模块
// #[cfg(test)] 这是一个属性宏, 用于条件编译(类似C语言条件编译)
// 告诉编译器, 只有在运行 cargo test 的时候才需要编译
#[cfg(test)]
pub mod statck_tests {
    // 这个模块的路径为: crate::linear::stack::stack_tests
    // 所以通过 super 关键字就可以直接找到 stack 模块中的 Stack 结构体
    use super::Stack;

    // #[tsst] 将一个方法标记为测试用例(test case)
    #[test]
    fn should_be_return_a_stack() {
        let stack: Stack<i32> = Stack::new();
        assert_eq!(stack.items.len(), 0);
        assert_eq!(stack.size(), 0);
        assert!(stack.is_empty());
    }

    #[test]
    fn should_be_empty_when_created() {
        let mut stack: Stack<i32> = Stack::new();
        assert!(stack.is_empty());

        stack.items.push(1);
        assert!(!stack.is_empty());
    }

    #[test]
    fn should_have_size_zero_when_created() {
        let mut stack: Stack<i32> = Stack::new();
        assert_eq!(stack.size(), 0);

        stack.items.push(1);
        assert_eq!(stack.size(), 1);
    }

    #[test]
    fn should_push_items_to_the_stack() {
        let mut stack: Stack<i32> = Stack::new();
        stack.push(5);
        assert_eq!(stack.items[0], 5);
        assert_eq!(stack.size(), 1);

        stack.push(4);
        assert_eq!(stack.items[1], 4);
        assert_eq!(stack.size(), 2);

        stack.push(3);
        assert_eq!(stack.items[2], 3);
        assert_eq!(stack.size(), 3);
    }

    #[test]
    fn should_pop_items_from_the_stack() {
        let mut stack: Stack<i32> = Stack::new();
        stack.push(5);
        stack.push(4);
        stack.push(3);

        assert_eq!(stack.size(), 3);

        // first in, last out
        assert_eq!(stack.pop(), Some(3));
        assert_eq!(stack.size(), 2);

        assert_eq!(stack.pop(), Some(4));
        assert_eq!(stack.size(), 1);

        assert_eq!(stack.pop(), Some(5));
        assert_eq!(stack.size(), 0);
    }

    #[test]
    fn should_peek_items_from_the_stack() {
        let mut stack: Stack<i32> = Stack::new();
        assert_eq!(stack.peek(), None);

        stack.push(5);
        assert_eq!(stack.peek(), Some(&5));

        // always return last item
        stack.push(4);
        assert_eq!(stack.peek(), Some(&4));
    }

    #[test]
    fn should_clear_items_from_the_stack() {
        let mut stack: Stack<i32> = Stack::new();
        stack.push(5);
        stack.push(4);
        stack.push(3);
        assert_eq!(stack.size(), 3);
        assert!(!stack.is_empty());

        stack.clear();
        assert_eq!(stack.size(), 0);
        assert!(stack.is_empty());
    }

}
rust
use unittests::linear::stack::Stack;

// 利用 stack 结构将10进制数转二进制字符串
// 转换规则如下:
// 1. 对2取余,获取余数
// 2. 对原来的数据 / 2 向下取整,直到数字为0
// 3. 重复1,2步骤,然后从后往前拼接余数
// 如下步骤: 将10进制的10转二进制
// 10 % 2 => 0, 10 / 2 => 5
// 5  % 2 => 1, 5  / 2 => 2
// 2  % 2 => 0, 2  / 2 => 1
// 1  % 2 => 1, 1  / 2 => 0
// 10进制的10转二进制就是 1010
fn convert_to_binary_str(n:i32) -> String {
    let mut stack: Stack<i32> = Stack::new();
    let mut binary_str = String::new();
    let mut decimal = n;
    let radix = 2;

    while decimal != 0 {
        stack.push(decimal % radix);
        decimal /= radix; // 默认就会向下取整
    }

    while !stack.is_empty() {
        let item = stack.pop().unwrap().to_string();
        binary_str.push_str(&item);
    }

    binary_str
}

fn main() {
    for i in 1..11 {
        let x = convert_to_binary_str(i);
        println!("i:{}, bin_str: {}", i, x);
    }
}
rust
pub mod linear {
    pub mod stack;
    // pub mod queue;
}

运行

使用在线10进制转二进制工具验证代码是否有误

sh
cargo test # 运行所有被标记为测试模块(#[cfg(test)])的测试用例(#[test])
cargo run  # 运行 main.rs 看代码是否有误

属性标注

rust
#[cfg(test)]    // 修饰模块, 将模块标记为测试模块
#[test]         // 修饰方法, 将方法标记为测试用例
#[should_panic] // 修饰方法, 将方法标记为测试用例, 并且只有当方法 pinic 时才通过测试
#[ignore]       // 修饰方法, 忽略被修饰的测试用例

断言宏

rust
assert!(x); // 只有 x 的值为 true 的时候才通过测试

assert_eq!(x, y); // 自由 x 与 y 相等的时候才通过测试

assert_ne!(x, y); // 自由 x 与 y 不相等的时候才通过测试

使用 Result 而不是断言宏来测试

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

#[cfg(test)]
mod tests {
    #[test]
    fn test_case_example() -> Result<(), String> {
        let x = sum(2, 2);
        if x == 4 {
            // ok 通过测试
            Ok(())
        } else {
            // err 不通过, 并输出信息到控制台
            Err(String::from("two plus two does not equal four"))
        }
    }
}

监听文件变化重新运行测试

JS 的单元测试框架(jest), 有个 --watch 选项, 可以让文件变化后, 自动重新运行所有测试, 但是很遗憾, Cargo 并没有这选项, 所以需要使用第三方工具 cargo-watch

sh
# 安装
cargo install cargo-watch
# or cargo binstall cargo-watch

# 运行测试(不会测试完后退出)
cargo watch -- cargo test

Released under the MIT License.