Rust之构建命令行程序(四):用TDD(测试-驱动-开发)模式来开发库的功能

开发环境

  • Windows 11
  • Rust 1.75.0 
  • VS Code 1.86.2

项目工程

这次创建了新的工程minigrep.

用测试-驱动模式来开发库的功能

 既然我们已经将逻辑提取到src/lib.rs中,并将参数收集和错误处理留在src/main.rs中,那么为代码的核心功能编写测试就容易多了。我们可以用各种参数直接调用函数并检查返回值,而不必从命令行调用我们的二进制文件。

在这一节中,我们将使用测试驱动开发(TDD)过程通过以下步骤向minigrep程序添加搜索逻辑:

  1. 编写一个失败的测试并运行它,以确保它因您预期的原因而失败。
  2. 编写或修改足够的代码以使新测试通过。
  3. 重构您刚刚添加或更改的代码,并确保测试继续通过。
  4. 从第1步开始重复!

尽管这只是编写软件的众多方法之一,但TDD可以帮助推动代码设计。在编写通过测试的代码之前编写测试有助于在整个过程中保持高测试覆盖率。

我们将测试该功能的实现,该功能将在文件内容中实际搜索查询字符串,并生成匹配查询的行列表。我们将在一个名为search的函数中添加此功能。

 编写失败的测试

 因为我们不再需要它们了,让我们把println!拿走吧!我们用来检查程序行为的来自src/lib.rs和src/main.rs的语句。然后,在src/lib.rs中,添加一个带有tests函数的测试模块,就像我们在之前的章节中所做的那样。测试函数指定了我们希望search函数具有的行为:它将获取一个查询和要搜索的文本,并且它将只返回包含该查询的文本行。示例12-15显示了这个测试,它还不能编译。

文件名:src/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));
    }
}

示例12-15:为我们希望拥有的search功能创建失败测试

该测试搜索字符串“duct”。我们正在搜索的文本有三行,其中只有一行包含“duct”(注意,左双引号后面的反斜杠告诉Rust不要在该字符串文字内容的开头放置换行符)。我们断言从search函数返回的值只包含我们期望的行。 

我们还不能运行这个测试并看着它失败,因为测试甚至没有编译:search功能还不存在!根据TDD原则,我们将通过添加一个总是返回一个空向量的search函数的定义来添加足够的代码来编译和运行测试,如示例12-16所示。那么测试应该会编译并失败,因为空向量与包含行“safe, fast, productive.”的向量不匹配。

文件名:src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

示例12-16:定义足够的search函数以便我们的测试可以编译

 请注意,我们需要在search的签名中定义一个显式的生命周期‘a',并将该生命周期与contents参数和返回值一起使用。回想一下之前章节,生存期参数指定哪个参数的生存期与返回值的生存期相关联。在这种情况下,我们指出返回的向量应该包含引用参数contents片段的字符串片段(而不是参数query)。

换句话说,我们告诉Rust,search函数返回的数据将与contents参数中传递给search函数的数据一样长。这很重要!切片引用的数据必须有效,引用才能有效;如果编译器认为我们正在生成query的字符串片段而不是contents片段,它将错误地进行安全检查。

如果我们忘记了生存期注释并试图编译该函数,我们将得到以下错误:

$ 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` due to previous error

 Rust不可能知道我们需要两个参数中的哪一个,所以我们需要明确地告诉它。因为contents是包含所有文本的参数,我们希望返回文本中匹配的部分,所以我们知道contents是应该使用生存期语法连接到返回值的参数。

其他编程语言不要求您将参数连接到签名中的返回值,但随着时间的推移,这种做法会变得越来越容易。

现在让我们运行测试:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [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 'assertion failed: `(left == right)`
  left: `["safe, fast, productive."]`,
 right: `[]`', src/lib.rs:44:9
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`

太好了,测试失败了,正如我们所料。让我们通过测试吧!

编写代码以通过测试

目前,我们的测试失败了,因为我们总是返回一个空向量。为了解决这个问题并实现search,我们的程序需要遵循以下步骤: 

  • 遍历每一行内容。
  • 检查该行是否包含我们的查询字符串。
  • 如果是的话,把它添加到我们返回的值列表中。
  • 如果没有,什么都不要做。
  • 返回匹配的结果列表。

让我们完成每一步,从遍历行开始。

使用lines方法遍历行

Rust有一个有用的方法来处理字符串的逐行迭代,方便地命名为lines,如示例12-17所示。注意这还不能编译。 

文件名:src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

示例12-17:遍历contents中的每一行

lines方法返回迭代器。我们将在后续章节深入讨论迭代器,但是回想一下你在示例3-5中看到了使用迭代器的这种方式,在那里我们使用了一个带有迭代器的for循环来对集合中的每一项运行一些代码。 

搜索查询的每一行

接下来,我们将检查当前行是否包含我们的查询字符串。幸运的是,字符串有一个名为contains的有用方法可以帮我们做到这一点!在search函数中添加对contains方法的调用,如示例12-18所示。请注意,这仍然不会编译。 

文件名:src/lib.rs

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
        }
    }
}

示例12-18:添加查看行是否包含query中的字符串的功能

目前,我们正在构建功能。为了让它编译,我们需要从主体返回一个值,就像我们在函数签名中指出的那样。 

存储匹配行

 为了完成这个函数,我们需要一种方法来存储我们想要返回的匹配行。为此,我for循环之前创建一个可变向量,并调用push方法在向量中存储一行。在for循环之后,我们返回向量,如示例12-19所示。

文件名:src/lib.rs

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
}

清单12-19:存储匹配的行以便我们可以返回它们

现在search函数应该只返回包含查询的行,我们的测试应该通过了。让我们进行测试:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [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函数已经运行并经过测试,我们需要从run函数中调用search。我们需要将config.query值和run从文件中读取的contents传递给search函数。然后run将打印search返回的每一行:

文件名:src/lib.rs

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(())
}

// 全部代码
use std::error::Error;
use std::fs;
 
pub struct Config {
    query: String,
    file_path: String,
}

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
}

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(())
}

我们仍然使用for循环从search中返回每一行并打印出来。

现在整个程序应该工作了!让我们试一试,首先用一个词来回答艾米莉·狄金森诗歌“青蛙”中的一行:

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [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 [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 [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

太棒了。我们已经构建了我们自己的经典工具的迷你版本,并学习了很多关于如何构建应用程序的知识。我们还学习了一些关于文件输入和输出、生存期、测试和命令行解析的知识。

为了完成这个项目,我们将简要演示如何使用环境变量以及如何打印到标准错误,这两者在您编写命令行程序时都很有用。

本章重点

  • 了解TDD概念
  • 如何使用TDD
  • 如何编写TDD案例和注意细节

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/405963.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Opencv实战(2)绘图与图像操作

Opencv实战(2)绘图与图像操作 指路前文&#xff1a;Opencv实战(1)读取与像素操作 三、基本绘图 文章目录 Opencv实战(2)绘图与图像操作三、基本绘图(1).line(2).rectangle(3).circle 四、图像处理(1).颜色空间1.意义2.cvtColor()3.inRange()4.适应光线 (2).形态操作1.腐蚀2.膨…

【软件架构】01-架构的概述

1、定义 软件架构就是软件的顶层结构 RUP&#xff08;统一过程开发&#xff09;4 1 视图 1&#xff09;逻辑视图&#xff1a; 描述系统的功能、组件和它们之间的关系。它主要关注系统的静态结构&#xff0c;包括类、接口、包、模块等&#xff0c;并用于表示系统的组织结构…

unity hub (第一部)初学配置

1、安装Unity Hub 2、设置中文 3、安装编辑器 4、新建项目 5、新建完成后进入编辑器 6、 编辑器设置中文 editPreferencesLanguages选择中文

mysql的日志文件在哪?

阅读本文之前请参阅----MySQL 数据库安装教程详解&#xff08;linux系统和windows系统&#xff09; MySQL的日志文件通常包括错误日志、查询日志、慢查询日志和二进制日志等。这些日志文件的位置取决于MySQL的安装和配置。以下是一些常见的日志文件位置和如何找到它们&#xff…

Android Studio基础(下载安装与简单使用)

1、搭建Android开发平台 1.1 Android Studio 下载地址及版本说明 Android 开发者官网&#xff1a; https://developer.android.com/index.html&#xff08;全球&#xff0c;需科学上网&#xff09; https://developer.android.google.cn/index.html&#xff08;国内&#xff…

【Flink精讲】Flink任务调度机制

Graph 的概念 Flink 中的执行图可以分成四层&#xff1a; StreamGraph -> JobGraph -> ExecutionGraph -> 物理执 行图。 StreamGraph&#xff1a;是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。JobGraph&#xff1a; StreamGraph …

Spring篇----第三篇

系列文章目录 文章目录 系列文章目录前言一、使用 Spring 有哪些方式?二、什么是 Spring IOC 容器?三、什么是依赖注入?四、可以通过多少种方式完成依赖注入?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这…

【MATLAB】 EWT信号分解+FFT傅里叶频谱变换组合算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~ 展示出图效果 1 EWT分解算法 EWT分解算法是一种基于小波变换的信号分解算法&#xff0c;它可以将信号分解为一系列具有不同频率特性的小波分量。该算法的基本思想是将信号分解为多个不同尺度的小波分量&#xff0c;并对…

了解网络延迟-MDN文档学习笔记

了解延迟 查看更多学习笔记&#xff1a;GitHub&#xff1a;LoveEmiliaForever MDN中文官网 CDN CDN (内容分发网络) 指的是一组分布在各个地区的服务器 这些服务器存储着数据的副本&#xff0c;因此服务器可以根据哪些服务器与用户距离最近&#xff0c;来满足数据的请求 CD…

BUUCTF crypto做题记录(8)新手向

一、密码学心声 得到信息如下图 背景故事没什么信息&#xff0c;主要看曲谱。大概率不会让我们涉及与音乐有关的内容&#xff0c;题目中也提示说答案是一串字符串&#xff0c;所以我们可以猜测是将曲谱上的数字转化成字符。曲谱中文字提示是用ASCII码进行转换。没有数字8可能是…

iOS面试:4.多线程GCD

一、多线程基础知识 1.1 什么是进程&#xff1f; 进程是指在系统中正在运行的一个应用程序。对于电脑而已&#xff0c;你打开一个软件&#xff0c;就相当于开启了一个进程。对于手机而已&#xff0c;你打开了一个APP&#xff0c;就相当于开启了一个进程。 1.2 什么是线程&am…

Java核心-核心类与API(2)

话接上回&#xff0c;继续核心类与API的学习&#xff0c;这次介绍StringBuffer/StringBuilder/StringJoiner类。StringBuffer和StringBuilder是我们学习的重点&#xff0c;建议对比学习&#xff0c;做好区分。 一、StringBuffer类 1、概述 1&#xff09;问题 由于 String 类…

Django学习记录04——靓号管理整合

1.靓号表 1.1 表结构 1.2 靓号表的构造 class PrettyNum(models.Model): 靓号表 mobile models.CharField(verbose_name"手机号", max_length11)# default 默认值# null true&#xff0c;blank true 允许为空price models.IntegerField(verbose_name"价…

websocket了解下

websocket请求长啥样 GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13 啥是websocket websocket是http的一种&#xff0c;服务器可以主动向客户端推送信息&#xff0c;…

18个惊艳的可视化大屏(第七辑):场馆与园区方向

本期分享智慧场馆和智慧园区方向的可视化大屏&#xff0c;各位老铁上车&#xff0c;坐稳了&#xff0c;上图啦。

计算机组成原理(12)----多处理系统

目录 1.SISD&#xff08;单指令流单数据流&#xff09; &#xff08;1&#xff09;特性 &#xff08;2&#xff09;硬件组成 2.SIMD&#xff08;单指令流多数据流&#xff09; &#xff08;1&#xff09;特性 &#xff08;2&#xff09;硬件组成 3.MISD&#xff08;多指…

数字孪生低代码平台盘点(一):厂家介绍

特别说明&#xff1a;本文根据网上资料搜集整理而成&#xff0c;排名不分先后&#xff0c;配图是为了更好地阅读体验&#xff0c;并非表明该图为该平台所生产。如有错误之处&#xff0c;请在评论区提出。 一、优锘ChartBuilder 优锘ChartBuilder是一款基于Web的数据可视化工具…

【Docker】初学者 Docker 基础操作指南:从拉取镜像到运行、停止、删除容器

在现代软件开发和部署中&#xff0c;容器化技术已经成为一种常见的方式&#xff0c;它能够提供一种轻量级、可移植和可扩展的应用程序打包和部署解决方案。Docker 是目前最流行的容器化平台之一&#xff0c;它提供了一整套工具和技术&#xff0c;使得容器的创建、运行和管理变得…

Github 2024-02-24 开源项目日报Top10

根据Github Trendings的统计&#xff0c;今日(2024-02-24统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Python项目5TypeScript项目2C项目1Rust项目1JavaScript项目1HTML项目1Jupyter Notebook项目1 Python - 100天…

华为配置WLAN AC和AP之间VPN穿越示例

配置WLAN AC和AP之间VPN穿越示例 组网图形 图1 配置WLAN AC和AP之间VPN穿越示例组网图 业务需求组网需求数据规划配置思路配置注意事项操作步骤配置文件 业务需求 企业用户接入WLAN网络&#xff0c;以满足移动办公的最基本需求。且在覆盖区域内移动发生漫游时&#xff0c;不影响…