前言
经过前面三节的学习,我们的小工具minigrep已经实现了读取指定文件内容,并且为了后期开发和测试的方便,重构了整个项目,使错误处理规整化,模块规范化。本次我们将采用测试驱动开发(以后简称TDD)
的模式进行开发,为程序编写几个程序测试用例,测试程序搜索查询字符串并返回匹配的行示例的功能,这些功能会在后面开发过程中用到。
测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。
一、任务目的
了解测试驱动开发模式(TDD)
,熟悉其开发步骤。使用TDD开发模式,编写我们所需要的测试功能代码,逐步增加软件的功能。
TDD是一个软件开发技术,它遵循如下步骤:
- 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
- 编写或修改足够的代码来使新的测试通过。
- 重构刚刚增加或修改的代码,并确保测试仍然能通过。
- 从步骤 1 开始重复!
使用TDD开发模式的好处
- 有助于驱动代码的设计
- 有助于在开发过程中保持高测试覆盖率
二、编写测试失败用例
1.增加测试模块和测试函数
我们仿照创建库时里面自带的测试代码,编写测试模块,在其中我们写了个one_result
函数用来测试,其中定义了query搜索关键词和contents内容,模拟我们实际操作中获取到的参数,调用了一个search
函数,将刚才的参数传入,并且断言
返回的就是关键词那一行的vector。
这里我们传入的关键词是芙蓉
,因此,如果search
运行正常的话就会返回芙蓉老秋霜,团扇羞网尘。戚姬髡发入舂市,万古共悲辛。
。
search
函数还没写,因此直接编译必然会报错,这里我们希望传入这两个值并且返回关键词所在的行才这么写的,search
函数的编写按照我们调用的样子来写。
#[cfg(test)] mod tests { use super::*; #[test] fn one_result() { let query = "芙蓉"; let contents = "\ 中山孺子妾,特以色见珍。虽然不如延年妹,亦是当时绝世人。 桃李出深井,花艳惊上春。一贵复一贱,关天岂由身。 芙蓉老秋霜,团扇羞网尘。戚姬髡发入舂市,万古共悲辛。"; assert_eq!(vec!["芙蓉老秋霜,团扇羞网尘。戚姬髡发入舂市,万古共悲辛。"], search(query, contents)); } }
2.编写search函数
由于这里我们是编写测试错误的用例,要确保程序出错是按照我们所期望的方式出错,因此这里我们在search函数返回一个空的vector,确保代码能够编译,且返回的不是我们所预期的结果,代码如下,
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] }
此时我们运行一下测试,结果返回断言的左值不等于右值
,说明我们写的代码是没有问题的,在后面我们会修复这个错误,让代码测试通过,如下图
三、修改代码,让代码测试通过
目前测试之所以会失败是因为我们总是返回一个空的 vector。为了让程序能够通过测试,我们需要完善search
函数的逻辑,返回正确的结果。search
的程序流程图如下
开始定义存储Vector是否到达尾行?结束是否包含关键词yesno
1. 按行读取
Rust提供了可以按行读取文本的方法lines
,他的调用方法是
contents.lines()
该方法返回一个数组,其中每一位元素都是文本内容的一行。我们用for循环来读取每一行,并且对每一行进行操作,所以对search
函数这样改动
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // 对文本行进行操作 } }
2. 检查关键字
检查关键字实际上就是查找字符串,Rust字符串也提供了可以查找字符串的方法contains
,他是这么调用的
contents.contains(keyword)
现在我们将他加入search
函数中
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // 对文本行进行操作 if line.contains(query) { } } }
3. 存储搜索结果
现在我们可以遍历完每一行,并且对每一行进行检查是否存在我们要找的关键字,所以现在要考虑的就是怎么把这些包含关键字的行保存并返回。考虑在for循环之外创建一个Vector,每当有符合条件的行就在for循环的判断中加入进去,代码如下
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 }
这里定义了一个可变的Vector类型的变量results
,然后在for循环中判断,如果有符合条件的行就把这行加到results
中,最后返回results
。
4. 运行测试
现在我们来运行一下这个测试用例,
可见我们写的search
函数是符合条件的,通过了测试。
四、在程序中使用代码
我们的项目主要逻辑都是放在run
函数中的,因此我们只需要在run
函数中调用search
函数,并输出每一行的内容就好了,以下是代码
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(()) }
此时运行程序来看看效果,
输入个比较短的关键字,查看是否能找到所有行
输入一个里面不存在的关键字
总结
现在我们就基本完成了这个小工具的开发,创建了个属于自己的小工具,学习了如何组织程序,驱动测试开发的开发方法,还有一些文件输入输出、生命周期、测试和命令行解析的内容。
到现在为止,这个小工具的主要功能就算是开发完毕了,后续我们将优化处理环境变量和输出标准内容,待续。
作业
到现在为止你已经基本完成这个小案列,请思考以下内容:
- 对于字符串的操作,比如字符串分割,字符串替换等怎么用Rust来写。
- 测试驱动开发有什么优点,有哪些步骤。