生活在不可避免地走向庸俗。--王小波
大家好,我是柒八九。
前言
在上一篇Rust 开发命令行工具(上)中我们从项目配置
/参数获取
/解析文件内容
/处理错误信息
/信息输出处理
等方面。一步一步写出来可以构建出在本地,兼容错误提示,并且有很好的输出形式的本地搜索工具。
以防大家遗忘,我们把最终的代码贴到下面。
use anyhow::{Context, Result}; use clap::Parser; use indicatif::ProgressBar; use std::fs::File; use std::io::{self, BufRead, Write}; use std::path::PathBuf; use std::thread; use std::time::Duration; #[derive(Parser)] struct Cli { /// 要查找的模式 pattern: String, /// 要读取的文件的路径 path: PathBuf, } fn main() -> Result<()> { let args = Cli::parse(); // 打开文件并创建一个 BufReader 来逐行读取 let file = File::open(&args.path).with_context(|| format!("无法打开文件 {:?}", &args.path))?; let reader = io::BufReader::new(file); let stdout = io::stdout(); let stdout_lock = stdout.lock(); let mut handle = io::BufWriter::new(stdout_lock); let pb = ProgressBar::new(100); for line in reader.lines() { do_hard_work(); pb.println(format!("[+] 查找到了 #{:?}项", line)); pb.inc(1); let line = line.with_context(|| "无法读取行")?; if line.contains(&args.pattern) { writeln!(handle, "{}", line)?; } } Ok(()) } fn do_hard_work() { thread::sleep(Duration::from_millis(250)); }
但是,作为一个功能完备的项目,我们还需要做单元测试/集成测试和打包发布。所以,今天我们就从这两面来继续完善我们的Rust
项目。
你能所学到的知识点
- 前置知识点
- 代码测试
- 打包并发布 Rust 项目
好了,天不早了,干点正事哇。
前置知识点
前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用。
单元测试 VS 黑盒测试
单元测试和黑盒测试(也叫集成测试)是两种不同的软件测试方法,它们旨在检查和验证软件的质量和功能,但它们的关注点、方法和目标有所不同。
单元测试(Unit Testing)
- 焦点:
单元测试
关注测试软件的最小功能单元
,通常是一个函数、方法或模块
。它的目标是验证这个功能单元是否按照预期工作,而不考虑其他组件。 - 测试者:通常由开发人员编写和执行。开发人员编写测试用例,用于检查函数、方法或模块的各种输入和边界条件。
- 可见性:单元测试通常具有对代码的白盒访问权限,测试者可以访问和检查被测试单元的内部实现细节,以编写更精确的测试用例。
- 目标:主要目标是验证单元的正确性,确保它们按照规范执行,并处理各种输入情况。
黑盒测试(Black Box Testing)
- 焦点:
黑盒测试
关注测试整个软件系统的功能,而不考虑内部实现。它的目标是验证系统是否按照规范的需求和功能规范工作。 - 测试者:可以由测试工程师或独立的测试团队执行。测试者不需要了解系统的内部实现,只需关注系统的输入和输出。
- 可见性:黑盒测试没有对系统的内部实现细节的了解。测试者只能访问系统的外部接口和功能。
- 目标:主要目标是验证系统是否满足其规范和需求,以及是否在各种输入和条件下表现正常。
在实际项目中,通常需要同时进行
单元测试
和黑盒测试
,以确保软件在各个层面上都具有高质量和可靠性。
Rust trait
在Rust
中,trait
是一种特殊的类型,它定义了某些类型的共享行为。trait
提供了一种方式来抽象和共享方法,类似于其他编程语言中的接口
。通过实现trait
,你可以为自定义类型定义通用的行为,使其能够与其他类型一起工作,从而提高了Rust
代码的可复用性和灵活性。
下面我们简单解释一下trait
的使用
- 定义
trait
:
我们可以使用trait
关键字来定义一个trait
,然后在其中声明方法签名。
trait Printable { fn print(&self); }
- 这个示例定义了一个名为
Printable
的trait
,它要求实现该trait
的类型必须包含一个名为print
的方法。 - 实现
trait
:
要使类型实现一个trait
,我们需要在类型的定义中使用impl
块来实现trait
中声明的方法。
struct MyStruct { data: i32, } impl Printable for MyStruct { fn print(&self) { println!("Data: {}", self.data); } }
- 在这个示例中,
MyStruct
类型实现了Printable
trait
,提供了print
方法的具体实现。 - 使用
trait
:
一旦你实现了一个trait
,我们可以在任何实现了该trait
的类型上调用trait
中定义的方法。例如:
let my_instance = MyStruct { data: 42 }; my_instance.print();
- 在这里,我们创建了一个
MyStruct
的实例并调用了print
方法。
总的来说,trait
是Rust
中用于实现抽象和共享行为的强大工具,它有助于编写可复用的代码,同时确保类型的安全性和一致性。通过合理使用trait
,我们可以编写更清晰、更灵活和更可维护的Rust
代码。
更详细的内容,可以参考我们之前写的Rust 泛型、trait 与生命周期
Rust的模块系统
Rust
的{模块系统|the module system},包括:
- 包(
Packages
):Cargo
的一个功能,它允许你构建、测试和分享crate
。Crates
:一个模块的树形结构,它形成了库或二进制项目。- 模块(
Modules
)和use
: 允许你控制作用域和路径的私有性。- 路径(
path
):一个命名例如结构体、函数或模块等项的方式
包和 crate
- 包(
package
) 是**提供一系列功能的一个或者多个crate
。**一个包会包含有一个Cargo.toml
文件,阐述如何去构建这些crate
。 crate
是一个二进制项或者库。crate root
是一个源文件,Rust
编译器以它为起始点,并构成你的crate
的根模块。
包中所包含的内容由几条规则来确立。
- 一个包中至多只能包含一个{库 crate|library crate};
- 包中可以包含任意多个{二进制 crate|binary crate};
- 包中至少包含一个
crate
,无论是库的还是二进制的。
输入命令 cargo new xxx
:当我们输入了这条命令,Cargo
会给我们的包
创建一个 Cargo.toml
文件。查看 Cargo.toml
的内容,会发现并没有提到 src/main.rs
,因为 Cargo
遵循的一个约定:
src/main.rs
就是一个与包同名的{二进制 crate|binary crate} 的crate
根。- 同样的,
Cargo
知道如果包目录中包含src/lib.rs
,则包带有与其同名的{库 crate|library crate},且src/lib.rs
是crate
根。
crate
根文件将由 Cargo
传递给 rustc
来实际构建库或者二进制项目。
如果一个包同时含有src/main.rs
和 src/lib.rs
,则它有两个 crate
:一个库和一个二进制项,且名字都与包相同。
通过将文件放在
src/bin
目录下,一个包可以拥有多个二进制crate
:每个src/bin
下的文件都会被编译成一个独立的{二进制 crate|binary crate}。
一个 crate
会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在多个项目之间共享
。
关于这块的内容,可以参考之前我们写的Rust之包、Crate和模块
crates.io
是个啥?
crates.io
是 Rust
编程语言社区的官方包管理和分发平台。它类似于其他编程语言中的包管理器,如 Python
的 PyPI
、JavaScript
的 npm
,用于帮助 Rust
开发者分享、发布和获取 Rust
代码库(也称为 "crates
")。
以下是 crates.io
的一些关键特点和功能:
- 包管理器:
crates.io
提供了一个中央存储库,用于托管Rust crates
。开发者可以使用cargo
,Rust
的包管理工具,轻松地下载、安装和管理这些crates
。 - 包发布:任何
Rust
开发者都可以将自己的Rust
代码库发布到crates.io
上,供其他人使用。这使得代码共享和开源社区合作更加容易。 - 版本控制:每个
crate
都有自己的版本号,允许开发者指定使用特定版本的crate
。这有助于确保代码的稳定性和可靠性。 - 依赖管理:
crates.io
允许crate
之间建立依赖关系,开发者可以在自己的项目中引入其他crates
作为依赖项,从而快速构建功能强大的应用程序。 - 搜索和浏览:
crates.io
提供了一个易于使用的网站,允许开发者搜索、浏览和查找他们需要的Rust crates
。网站还提供了有关每个crate
的详细信息、文档和示例代码。 - 社区驱动:
crates.io
是由Rust
社区维护和支持的,任何人都可以为平台的发展和改进做出贡献。
总之,crates.io
是 Rust
生态系统的核心组成部分,它使 Rust
开发更加便捷,促进了 Rust
社区的增长和分享代码的文化。开发者可以在上面找到各种各样的 Rust crates
,以加速他们的项目开发。
2. 代码测试
为了确保我们的程序按照我们的期望工作,最明智的做法是对其进行测试。
一种简单的方法是编写一个README
文件,描述我们的程序应该执行的操作。当我们准备发布新版本时,通过README
可以描述我们程序的功能和行为。与此同时,我们还可以通过写下程序应该如何应对错误输入来让我们的程序变的更加严谨。
自动化测试
在
Rust
中,#[test]
是一个属性(attribute
),用于标记测试函数。Rust
内置了一个测试框架,可以使用这个属性来定义和运行测试。
以下是使用 #[test]
的基本步骤:
- 首先,确保我们的
Rust
项目是一个可测试的项目。通常,Rust
项目的测试代码存放在一个名为tests
的目录中,或者在我们的代码中使用条件编译来区分测试代码和生产代码。它允许构建系统发现这些函数并将其作为测试运行,验证它们不会触发panic
。 - 创建一个测试函数并标记为
#[test]
。测试函数必须返回()
(unit类型),并且通常不带参数。
#[test] fn test_example() { // 在这里编写测试代码 }
- 在测试函数中编写测试代码,包括调用我们要测试的函数,并使用断言来检查函数的输出是否与预期值匹配。我们可以使用标准库中的
assert!
宏或其他测试断言宏来进行断言。
#[test] fn test_addition() { assert_eq!(2 + 2, 4); } #[test] fn test_subtraction() { assert!(5 - 3 > 0); }
- 运行测试。可以使用
Rust
的测试运行器工具来执行测试。常见的测试命令是cargo test
,它会自动查找和运行项目中的所有测试函数。在项目根目录下运行以下命令:
cargo test
- 测试运行结果会显示在终端中。成功的测试将显示为
ok
,失败的测试将显示为fail
,并提供失败的详细信息,包括测试函数的名称和失败的断言。我们可以根据这些信息来调试和修复代码。 - 如果需要更详细的输出,可以使用
--verbose
标志运行测试
cargo test --verbose
我们应该最终得到类似以下的输出:
通过#[test]
我们可以测试我们想测试的核心代码,但是,作为一个CLI
通常不仅仅是一个函数,它需要更多的人机交互,例如需要处理用户输入、读取文件和编写输出等,我们不可预知的参数和行为。