网络知识 娱乐 【一起学Rust · 项目实战】命令行IO项目minigrep——测试驱动开发完善功能

【一起学Rust · 项目实战】命令行IO项目minigrep——测试驱动开发完善功能

文章目录

  • 前言
  • 一、任务目的
  • 二、编写测试失败用例
    • 1.增加测试模块和测试函数
    • 2.编写search函数
  • 三、修改代码,让代码测试通过
    • 1. 按行读取
    • 2. 检查关键字
    • 3. 存储搜索结果
    • 4. 运行测试
  • 四、在程序中使用代码
  • 总结
  • 作业


前言

经过前面三节的学习,我们的小工具minigrep已经实现了读取指定文件内容,并且为了后期开发和测试的方便,重构了整个项目,使错误处理规整化,模块规范化。本次我们将采用测试驱动开发(以后简称TDD)的模式进行开发,为程序编写几个程序测试用例,测试程序搜索查询字符串并返回匹配的行示例的功能,这些功能会在后面开发过程中用到。

测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。


一、任务目的

了解测试驱动开发模式(TDD),熟悉其开发步骤。使用TDD开发模式,编写我们所需要的测试功能代码,逐步增加软件的功能。

TDD是一个软件开发技术,它遵循如下步骤:

  1. 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
  2. 编写或修改足够的代码来使新的测试通过。
  3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
  4. 从步骤 1 开始重复!

使用TDD开发模式的好处

  1. 有助于驱动代码的设计
  2. 有助于在开发过程中保持高测试覆盖率

二、编写测试失败用例

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的程序流程图如下

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来写。
  • 测试驱动开发有什么优点,有哪些步骤。