为什么要编写测试?
TDD 的开发方式好处很多, 如:
- 修改代码后及时发现错误
- 小步走, 快速验证
- 确定被测试代码的输入输出, 解耦合
如何编写自动化测试
做个小案例:
- 利用 Vector 实现 Stack 数据结构, 编写单元测试, 验证 Stack 数据是否处理正确
- 利用 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