观看B站软件工艺师杨旭的rust教程学习记录,有删减有补充
自动化测试
测试:验证非测试代码功能是否和预期一致
测试函数体(3A操作)
- 准备数据/状态(Arrange)
- 运行被测代码(Act)
- 断言结果(Asstert)
- 在函数上加
#[test]
即可将函数变为测试函数 -
cargo test
Rust会构建一个Runner可执行文件,自动运行标注了#[test]
的测试函数
new一个名为hello的library项目
cargo new hello --lib
lib.rs内容
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);//判断两者是否相等
}
}
- 每个测试运行在一个新线程
-
panic!
宏会导致测试失败
Assert!宏
- 状态为true:测试通过
- 状态为false:调用panic!宏,测试失败
assert!
:里面可以指定错误信息
assert_eq!
: ==
assert_ne!
: !=
断言失败自动打印两个参数的值
fn add_two(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_teo() {
let result = add_two(2, 3);
assert!(result.eq(&6), "不等于6!"); //断言结果不等于6,并指定错误信息
assert_eq!(result, 5); // 断言结果等于 5,如果不等于则会导致程序出错
assert_ne!(result, 0); // 断言结果不等于 0,如果等于则会导致程序出错
}
}
- 使用
debug
格式打印参数 - 要求实现了PartialEq和Debug Trait(所有基本类型和大部分标准库类型都已实现)
debug_assert!宏
仅debug模式下运行
debug_assert!
:指定错误信息
debug_assert_eq!
:==debug_assert_ne!
:!=
should_panic
- 函数panic:测试通过
- 函数没有panic:测试失败
#[cfg(test)]
mod tests {
#[test]
#[should_panic(expected = "index out of bounds")]//只有信息中包含指定信息的panic才会测试通过
fn test_vector_index() {
let v = vec![1, 2, 3];
let _ = v[10];//超出索引,这个测试发生了panic所以测试通过
}
}
Result<(),T>
不会发生panic,失败返回Err,不要添加#[should_panic]
#[cfg(test)]
mod tests {
#[test]
fn it_work() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("结果不相等"))
}
}
}
控制测试
默认行为
- 并行运行
- 所有测试
- 捕获所有输出
参数控制
- cargo test 参数
- –help:帮助信息
- – – help:可以跟在cargo test --后面的参数
- 如
cargo test --no-run
:编译测试函数但不执行
- 可执行测试文件 参数
- 如
cargo test -- --show-output
:显示成功测试的输出
- 如
并行运行测试
-
运行多个测试默认使用多线程测试
-
需要确保测试之间不会相互依赖
-
不依赖与共享状态(环境、工作目录、环境变量等)
-
使用
cargo test -- --test=threads=1
指定线程数量
指定函数名测试
cargo test 函数名
:仅执行指定的测试函数
- 如
cargo test add_two_two
cargo test 模块名
:仅执行指定模块的测试
- 如
cargo test work
cargo test add
:仅执行开头包含add的测试函数
cargo test -- --ignored
:仅执行被忽略的测试函数,通过在测试函数上添加#[ignore]
忽略测试函数(比较耗时的函数可以忽略单独执行)
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]//指定单元测试
mod work {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2), "2+2");
}
#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3), "3+2");
}
#[test]
#[ignore = "reason"]//指定忽略原因
fn one_hundred() {
assert_eq!(102, add_two(100), "102+100");
}
}
测试分类(仅libary crate,binary crate也就是main.rs可独立运行不测试)
-
单元测试:
#[cfg(test)]
标注- 一次对一个模块进行隔离测试
- 可测试private函数(没有使用pub的函数默认是private的)
-
集成测试:创建
tests
目录,tests
目录下每个测试文件都是一个单独的crate
- 在库外部测试
- 只能测试public函数
- 可以在测试中使用多个模块
tests
文件夹下新建任意名字的测试文件use hello; #[test] fn it_adds_two() { assert_eq!(34,hello::add_two(32)); }
在tests
目录下新建子目录common
可以创建公共测试函数,通过use
导入,但不会被集成测试
程序关注点分离指导性原则
- 将程序拆分为
main.rs
和lib.rs
将业务逻辑放入lib.rs
- 纵向分离:界面层(
UI Layer
),业务逻辑层(Business Layer
)和数据持久化层(Data Access Layer
) - 横向分离:将软件拆分成模块(
module
)或子系统(crate
),每个子系统都有明确定义的接口和职责 - 切面分离:像日志在多个层都需要,通过配置日志过滤器和输出格式来控制日志的行为
- 依赖分离:使用依赖注入(DI)或依赖倒置(DI)原则,将组件的依赖关系从具体实现中解耦。可以使用 Rust 的依赖注入框架(如
di
)或手动实现依赖注入 - 关注数据分离:用数据结构和类型来表示和操作数据,确保数据的独立性和可重用性,如
struct
、enum
、泛型
等 - 关注行为分离:使用 Rust 的特质(trait)来定义抽象行为和接口,然后为不同的类型实现这些特质,这样可以将行为从具体类型中分离出来,增加了代码的灵活性和可重用性
- 扩展分离:使用模块和
trait
来支持可插拔的扩展功能 - 委托分离:将某个对象的功能委托给其他对象来实现
- 反转分离:使用依赖反转原则(DIP)或控制反转(IOC)容器来实现
main.rs
use std::{env, process};
use minigrep::{run, Config};
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
let config = Config::new(&args).unwrap_or_else(|err| {
println!("解析参数出错:{}", err);
process::exit(1); //状态码推出
});
println!("查找:{}", config.query);
println!("文件:{}", config.filename);
if let Err(e) = run(config) {
println!("程序出错!:{}", e);
process::exit(1);
}
}
lib.rs
use std::error::Error;
use std::fs;
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
println!("{}", contents);
Ok(())
}
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
//错误处理
if args.len() < 3 {
return Err("参数不够!");
}
let query = &args[1].clone();
let filename = &args[2].clone();
Ok(Config {
query: query.to_string(),
filename: filename.to_string(),
})
}
}
test.txt
Rust:
hello world
so easy
TDD(Test-Driven Development)
TDD:开发功能代码之前,先编写单元测试用例代码
TDD包含以下三个方面,一般指的是UTDD
UTDD
(Unit Test Driven Development,单元测试驱动开发)
ATDD
(Acceptance Test Driven Development,验收测试驱动开发)
BDD
(Behavior Driven Development,行为驱动测试开发)
TDD周期
测试先行->迭代开发->持续重构
- 写一个会失败的测试,运行测试确保它按照预期的原因失败(
Test Fails
红) - 编写或修改刚好足够的代码,让测试通过(
Test Passes
绿) - 重构优化设计,确保测试始终通过(
Refactor
重构),重复该过程
TDD原则
- 除非是为了一个失败的用例通过,否则不允许编写任何代码
- 在一个单元测试中,只允许编写刚好能导致失败的内容
- 只允许编写刚好能够使一个失败用例通过的代码
1、先写一个测试
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {//为了让一个失败的用例通过
vec![]
}
//TDD
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "hello";
let contents = "
Rust
hello world
";
assert_eq!(vec!["hello world"], search(query, contents))
}
}
执行cargo test
出现FAILED
(红)
2、编写或修改刚好通过测试的代码
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
//为了让一个失败的用例通过
let mut results = Vec::new();
for line in contents.lines() {
//将每一行遍历
if line.contains(query) {
results.push(line); //包含查询结果则存入results
}
}
results
}
执行cargo test
出现ok
(绿)
3、重构优化设计,确保测试始终通过(修改lib.rs中的run方法)
use std::error::Error;
use std::fs;
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
for line in search(&config.query, &contents) {//重构方法
println!("{}", line);
}
Ok(())
}
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
//错误处理
if args.len() < 3 {
return Err("参数不够!");
}
let query = &args[1].clone();
let filename = &args[2].clone();
Ok(Config {
query: query.to_string(),
filename: filename.to_string(),
})
}
}
执行cargo run hello test.txt
确保测试通过
closures 闭包
可以捕获其所在环境的匿名函数
fn apply_operation<F>(a: i32, b: i32, operation: F) -> i32
where
F: Fn(i32, i32) -> i32,
{
operation(a, b)
}
fn main() {
let add = |a, b| a + b;
let result = apply_operation(2, 3, add); //将闭包函数作为参数使用
println!("Result: {}", result); //Result: 5
}
闭包获得函数的三种方式
- 取得所有权:
FnOnce
- 可变借用:
FnMut
- 不可变借用:
Fn
FnOnce
取得所有权(move
)
fn main() {
let name = String::from("Alice");
let greet = move || {
println!("Hello, {}", name);
};
greet();
// 下面这行代码将无法编译,因为闭包已经获取了 `name` 的所有权
// println!("Name: {}", name);
}
FnMut
可变借用
fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("Count: {}", count);
};
increment();
increment();
}
Fn
不可变借用
fn main() {
let name = "Bob";
let greet = || {
println!("Hello, {}", name);
};
greet();
greet();
}
iterators 迭代器
检查容器内元素并遍历元素
Rust的迭代器是惰性的,只有调用时才能显示
fn main() {
let v = vec![1, 2, 3, 4];
let v = v.iter();
for val in v {
println!("迭代数据:{}", val);
}
}
其他用法
fn main() {
let shoe_sizes = vec![8, 9, 10, 9, 8, 7, 8, 9];
// 使用迭代器的 next 方法遍历元素
let mut sizes_iterator = shoe_sizes.iter();
while let Some(size) = sizes_iterator.next() {
println!("size: {}", size);
}
// 使用迭代器的消费方法进行求和
let sum: i32 = shoe_sizes.iter().sum();
println!("求和sizes: {}", sum);//求和sizes: 68
// 使用迭代器适配器进行筛选,filter返回true则将元素包含在filter产生的迭代器中
let small_sizes: Vec<&i32> = shoe_sizes.iter().filter(|&size| size < &8).collect();
println!("最小的sizes: {:?}", small_sizes);//最小的sizes: [7]
// 使用捕获 shoe_size 的闭包进行筛选
let shoe_size = 9;
let matching_sizes: Vec<&i32> = shoe_sizes
.iter()
.filter(|&size| size == &shoe_size)
.collect();
println!("匹配的sizes: {:?}", matching_sizes);//匹配的sizes: [9, 9, 9]
}
零成本抽象
不用的东西,你不需要为之付出代价,用到的东西,你也不可能做得更好
能带来感官上高层次的抽象又不会带来运行时的性能损失
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// 使用闭包迭代器计算所有奇数的平方和
let odd_square_sum: i32 = numbers
.iter()
.filter(|&num| num % 2 != 0)
.map(|&num| num * num)
.sum();
println!("Sum of squares of odd numbers: {}", odd_square_sum);