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

2024-02-24 10:36

本文主要是介绍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 buildCompiling 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 testCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished test [unoptimized + debuginfo] target(s) in 0.97sRunning unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)running 1 test
test tests::one_result ... FAILEDfailures:---- 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 backtracefailures:tests::one_resulttest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00serror: 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 testCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished test [unoptimized + debuginfo] target(s) in 1.22sRunning unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)running 1 test
test tests::one_result ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sRunning unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)running 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests minigreprunning 0 teststest 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.txtCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.38sRunning `target/debug/minigrep frog poem.txt`
How public, like a frog

 酷!现在让我们尝试一个可以匹配多行的单词,例如“body”:

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

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

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

本章重点

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

这篇关于Rust之构建命令行程序(四):用TDD(测试-驱动-开发)模式来开发库的功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/741834

相关文章

使用Python构建智能BAT文件生成器的完美解决方案

《使用Python构建智能BAT文件生成器的完美解决方案》这篇文章主要为大家详细介绍了如何使用wxPython构建一个智能的BAT文件生成器,它不仅能够为Python脚本生成启动脚本,还提供了完整的文... 目录引言运行效果图项目背景与需求分析核心需求技术选型核心功能实现1. 数据库设计2. 界面布局设计3

深入浅出SpringBoot WebSocket构建实时应用全面指南

《深入浅出SpringBootWebSocket构建实时应用全面指南》WebSocket是一种在单个TCP连接上进行全双工通信的协议,这篇文章主要为大家详细介绍了SpringBoot如何集成WebS... 目录前言为什么需要 WebSocketWebSocket 是什么Spring Boot 如何简化 We

PyQt5 GUI 开发的基础知识

《PyQt5GUI开发的基础知识》Qt是一个跨平台的C++图形用户界面开发框架,支持GUI和非GUI程序开发,本文介绍了使用PyQt5进行界面开发的基础知识,包括创建简单窗口、常用控件、窗口属性设... 目录简介第一个PyQt程序最常用的三个功能模块控件QPushButton(按钮)控件QLable(纯文本

Linux之platform平台设备驱动详解

《Linux之platform平台设备驱动详解》Linux设备驱动模型中,Platform总线作为虚拟总线统一管理无物理总线依赖的嵌入式设备,通过platform_driver和platform_de... 目录platform驱动注册platform设备注册设备树Platform驱动和设备的关系总结在 l

Java实现预览与打印功能详解

《Java实现预览与打印功能详解》在Java中,打印功能主要依赖java.awt.print包,该包提供了与打印相关的一些关键类,比如PrinterJob和PageFormat,它们构成... 目录Java 打印系统概述打印预览与设置使用 PageFormat 和 PrinterJob 类设置页面格式与纸张

MySQL 8 中的一个强大功能 JSON_TABLE示例详解

《MySQL8中的一个强大功能JSON_TABLE示例详解》JSON_TABLE是MySQL8中引入的一个强大功能,它允许用户将JSON数据转换为关系表格式,从而可以更方便地在SQL查询中处理J... 目录基本语法示例示例查询解释应用场景不适用场景1. ‌jsON 数据结构过于复杂或动态变化‌2. ‌性能要

Spring Boot Maven 插件如何构建可执行 JAR 的核心配置

《SpringBootMaven插件如何构建可执行JAR的核心配置》SpringBoot核心Maven插件,用于生成可执行JAR/WAR,内置服务器简化部署,支持热部署、多环境配置及依赖管理... 目录前言一、插件的核心功能与目标1.1 插件的定位1.2 插件的 Goals(目标)1.3 插件定位1.4 核

基于Python开发一个图像水印批量添加工具

《基于Python开发一个图像水印批量添加工具》在当今数字化内容爆炸式增长的时代,图像版权保护已成为创作者和企业的核心需求,本方案将详细介绍一个基于PythonPIL库的工业级图像水印解决方案,有需要... 目录一、系统架构设计1.1 整体处理流程1.2 类结构设计(扩展版本)二、核心算法深入解析2.1 自

使用Python构建一个高效的日志处理系统

《使用Python构建一个高效的日志处理系统》这篇文章主要为大家详细讲解了如何使用Python开发一个专业的日志分析工具,能够自动化处理、分析和可视化各类日志文件,大幅提升运维效率,需要的可以了解下... 目录环境准备工具功能概述完整代码实现代码深度解析1. 类设计与初始化2. 日志解析核心逻辑3. 文件处

golang程序打包成脚本部署到Linux系统方式

《golang程序打包成脚本部署到Linux系统方式》Golang程序通过本地编译(设置GOOS为linux生成无后缀二进制文件),上传至Linux服务器后赋权执行,使用nohup命令实现后台运行,完... 目录本地编译golang程序上传Golang二进制文件到linux服务器总结本地编译Golang程序