使用测试驱动开发开发库的功能
现在我们已经将逻辑提取到 src/lib.rs 中并留下了参数 在 src/main.rs 中收集和错误处理,编写测试要容易得多 了解我们代码的核心功能。我们可以直接使用 各种参数并检查返回值,而无需调用我们的二进制文件 从命令行。
在本节中,我们将向minigrep
程序使用
测试驱动开发 (TDD) 流程,步骤如下:
- 编写一个失败的测试并运行它,以确保它失败的原因 期望。
- 编写或修改刚好足以使新测试通过的代码。
- 重构您刚刚添加或更改的代码,并确保测试继续 通过。
- 从第 1 步开始重复!
虽然 TDD 只是编写软件的众多方法之一,但它可以帮助驱动代码 设计。在编写使测试通过的代码之前编写测试 有助于在整个过程中保持较高的测试覆盖率。
我们将试驾实际执行的功能的实现
在文件内容中搜索查询字符串,并生成一个
与查询匹配的行。我们将在名为search
.
编写失败的测试
因为我们不再需要它们,所以让我们删除println!
来自 src/lib.rs 和 src/main.rs 的语句,我们用它来检查程序的行为。
然后,在 src/lib.rs 中,我们将添加一个tests
模块中,因为我们
在第 11 章中。test 函数指定
我们想要search
函数:它将需要一个 query 和
要搜索的文本,它将仅返回文本中
包含查询。示例 12-15 显示了这个测试,它还不会编译。
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
Ok(())
}
#[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));
}
}
search
我们希望拥有的功能此测试搜索字符串"duct"
.我们正在搜索的文本是 3
行,其中只有一个包含"duct"
(请注意,在
左双引号告诉 Rust 不要在开头放置换行符
)的内容。我们断言从
这search
function 仅包含我们期望的行。
我们还无法运行此测试并看着它失败,因为测试没有
even compile:search
函数尚不存在!符合 TDD
原则,我们将添加足够的代码来编译和运行测试
添加search
函数,该函数始终返回一个空的
vector,如示例 12-16 所示。然后测试应该编译并失败
因为空向量与包含该行的向量不匹配"safe, fast, productive."
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
#[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));
}
}
search
函数,因此我们的测试将编译请注意,我们需要定义一个显式的生命周期'a
在search
并将该生命周期与contents
参数和 return
价值。回想一下第 10 章中,生命周期
parameters 指定哪个参数的生命周期与
返回值。在这种情况下,我们指示返回的 vector 应包含
引用参数的 slices 的 string 切片contents
(而不是
论点query
).
换句话说,我们告诉 Rust 由search
功能
只要传递到search
函数中的contents
论点。这很重要!切片引用的数据需要
有效,引用才有效;如果编译器假定我们正在进行
字符串切片query
而不是contents
,它将进行安全检查
错误。
如果我们忘记了 lifetime 注解并尝试编译这个函数,我们将 收到此错误:
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:28:51
|
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error
Rust 不可能知道我们需要两个参数中的哪一个,所以我们需要告诉
它明确地。因为contents
是包含我们所有文本的参数
并且我们想要返回该文本中匹配的部分,我们知道contents
是
应使用 lifetime 连接到返回值的参数
语法。
其他编程语言不需要你连接参数来返回 值,但随着时间的推移,这种做法会变得更容易。你可以 希望将此示例与“验证引用 with Lifetimes“ 部分 在第 10 章中。
现在让我们运行测试:
$ 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`
太好了,测试失败了,正如我们预期的那样。让我们通过测试吧!
编写代码以通过测试
目前,我们的测试失败,因为我们总是返回一个空 vector。修复
并实施search
,我们的程序需要遵循以下步骤:
- 遍历内容的每一行。
- 检查该行是否包含我们的查询字符串。
- 如果是,请将其添加到我们返回的值列表中。
- 如果没有,则什么都不做。
- 返回匹配的结果列表。
让我们完成每个步骤,从迭代行开始。
使用lines
方法
Rust 有一个有用的方法来处理字符串的逐行迭代,
命名方便lines
,其工作原理如图 12-17 所示。请注意,
这还不会编译。
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
#[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));
}
}
contents
这lines
method 返回一个迭代器。我们将在第 13 章中深入讨论迭代器,但请记住,您是这样看到的
在示例 3-5 中使用迭代器,其中我们使用了for
循环,以对集合中的每个项目运行一些代码。
在每一行中搜索查询
接下来,我们将检查当前行是否包含我们的查询字符串。
幸运的是,字符串有一个名为contains
这样做
我们!添加对contains
方法中的search
函数,如
示例 12-18.请注意,这仍然不会编译。
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
#[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
目前,我们正在构建功能。为了让代码进行编译,我们 需要从 body 返回一个值,正如我们在函数中指示的那样 签名。
存储匹配行
要完成这个功能,我们需要一种方法来存储我们想要的匹配行
返回。为此,我们可以在for
loop 和
调用push
方法来存储line
在向量中。在for
圈
我们返回 vector,如示例 12-19 所示。
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
Ok(())
}
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
}
#[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));
}
}
现在,search
function 应仅返回包含query
,
我们的测试应该会通过。让我们运行测试:
$ 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
我们的测试通过了,所以我们知道它有效!
此时,我们可以考虑重构 实现 search 函数,同时保持测试传递到 保持相同的功能。search 函数里的代码还不错, 但它没有利用迭代器的一些有用功能。我们将 返回第 13 章中的这个例子,其中 我们将详细探讨迭代器,并了解如何改进它。
使用search
函数中的run
功能
现在,search
函数正在运行并经过测试,我们需要调用search
从我们的run
功能。我们需要将config.query
value 和contents
那run
从文件读取到search
功能。然后run
将打印从search
:
文件名: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
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
}
#[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));
}
}
我们仍在使用for
loop 返回每行search
并打印它。
现在整个程序应该可以工作了!让我们试一试,首先使用一个词 应该正好返回艾米莉·狄金森 (Emily Dickinson) 诗中的一行:frog。
$ 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
凉!现在让我们尝试一个匹配多行的单词,例如 body:
$ 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!
最后,让我们确保在搜索 诗中任何位置都没有的单词,例如 monomorphization:
$ 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`
非常好!我们已经构建了自己的经典工具的迷你版本,并学到了很多东西 了解如何构建应用程序。我们还了解了一些关于文件输入的知识 以及 output、lifetimes、testing 和 command line 解析。
为了完善这个项目,我们将简要演示如何使用 环境变量和如何打印为标准错误,这两者都是 在编写命令行程序时很有用。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准