12.6.0. 写在正文之前
第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep
(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。
这个项目分为这么几步:
- 接收命令行参数
- 读取文件
- 重构:改进模块和错误处理
- 使用TDD(测试驱动开发)开发库功能(本文)
- 使用环境变量
- 将错误信息写入标准错误而不是标准输出
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
12.6.1. 回顾
以下是截止到上一篇文章为止所写出的全部代码。
lib.rs
:
use std::error::Error;
use std::fs;
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("Not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config { query, filename})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}
main.rs
:
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args:Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
在前几节中我们完成了对业务逻辑的迁移,把它分离到lib.rs
里。这样对编写测试帮助很大,因为lib.rs
中的逻辑不需要在命令行下运行就可以直接使用不同的参数调用业务功能函数,并校验其返回值,也就是针对业务逻辑进行测试。
12.6.2. 什么是测试驱动开发TDD
TDD是Test-Driven Development的缩写,中文名为测试驱动开发,一般遵循以下步骤:
- 编写一个会失败的测试,运行该测试,并确保它是按照预期的原因失败
- 编写或修改刚好足够的代码,让新测试通过
- 重构刚刚添加或修改的代码,确保测试会通过
- 返回步骤1,继续
TDD只是众多软件开发方法中的一种,但是它能对代码的设计工作起到指导和帮助的作用。先编写测试,然后再编写能够通过测试的代码也有助于开发过程中保持较高的测试覆盖率。
本篇文章会通过测试驱动开发的步骤完成程序的搜索逻辑——在文件内容中搜索指定的字符串,将符合的内容的行数放在一个列表中。这个函数会被命名为search
。
12.6.3. 修改代码
按照TDD的步骤来写代码:
1. 编写会失败的测试
首先到lib.rs
里编写一个测试模块:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."],search(query, contents));
}
}
也就是说,因为query
存储的"duct"在"safe, fast, productive.“这一行,所以返回值会是元素为String
的Vector
,并且只有一个元素,内容会是"safe, fast, productive.”
返回值是Vector
是因为search
函数预期能处理多个符合的结果,当然这个测试函数只可能有一个结果,这个测试函数取名叫one_result
也是因为如此。
写好了测试模块,接下来写search
函数:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
- 为了让这个函数能被外部函数调用,得使用
pub
来声明为公共的 - 这个函数得加生命周期标志,因为有多个非
self
的参数,Rust无法判断哪个参数的生命周期跟返回值的生命周期相同。 - 返回值
Vector
内的元素是字符串切片,是从contents
截取的,所以返回值应和contents
的生命周期相同,所以给它们两个标注了一样的生命周期'a
,而query
则不需要生命周期标注。 - 函数内容只需要确保能通过编译即可,因为TDD的第一步是编写一个会出错的测试,所以出错才是想要的结果。
测试结果:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.97s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... FAILED
failures:
---- tests::one_result stdout ----
thread 'tests::one_result' panicked at src/lib.rs:44:9:
assertion `left == right` failed
left: ["safe, fast, productive."]
right: []
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
这个测试失败了,但没问题,这就是TDD第一步想要的结果。
2. 编写或修改刚好足够的代码,让新测试通过
第一步完成,接下来执行TDD的第二步:编写或修改刚好足够的代码,让新测试通过。
思考search
的思路,应该是遍历contents
的每一行,在遍历的时候查找是否有符合query
的字符串,有就把这一行放到返回值的列表中;如果没有,什么都不做,遍历下一行。最后把所有结果放到Vector
里返回即可。
-
对于遍历每一行,可以使用
lines
方法,它会返回一个迭代器(13章会细讲),会把字符串的内容一行一行地返回。 -
对于查找是否有符合
query
的字符串,可以使用contains
方法,它返回的是一个布尔类型,有符合的就返回true
,反之则为false
。 -
最后别忘了,要把符合的行放到
Vector
里。
根据以上这些知识,就可以写出代码了:
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
不用显示声明元素类型是因为下文中往这个Vector
里添加了line
这个&str
类型,Rust推断出results
里的元素类型是&str
。
现在运行一下测试:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
测试通过,没有问题。
3. 在run
函数中使用search
函数
search
函数目前写好了,那就可以在run
函数中调用了:
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(())
}
通过循环的方式找到符合的一行就立马打印出来。
试运行一下:
$ cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
这个例子只有单行,试试有多行的字符:
$ cargo run -- body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
试一个没有的词汇:
$ cargo run -- monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`