Rust单元测试、集成测试

单元测试、集成测试

在了解了如何在 Rust 中写测试用例后,本章节我们将学习如何实现单元测试、集成测试,其实它们用到的技术还是上一章节中的测试技术,只不过对如何组织测试代码提出了新的要求。

单元测试

单元测试目标是测试某一个代码单元(一般都是函数),验证该单元是否能按照预期进行工作,例如测试一个 add 函数,验证当给予两个输入时,最终返回的和是否符合预期。

在 Rust 中,单元测试的惯例是将测试代码的模块跟待测试的正常代码放入同一个文件中,例如 src/lib.rs 文件中有如下代码:

rust 复制代码
pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(add_two(2), 4);
    }
}

add_two 是我们的项目代码,为了对它进行测试,我们在同一个文件中编写了测试模块 tests,并使用 #[cfg(test)] 进行了标注。

条件编译 #[cfg(test)]

上面代码中的 #[cfg(test)] 标注可以告诉 Rust 只有在 cargo test 时才编译和运行模块 tests,其它时候当这段代码是空气即可,例如在 cargo build 时。这么做有几个好处:

  • 节省构建代码时的编译时间
  • 减小编译出的可执行文件的体积

其实集成测试就不需要这个标注,因为它们被放入单独的目录文件中,而单元测试是跟正常的逻辑代码在同一个文件,因此必须对其进行特殊的标注,以便 Rust 可以识别。

#[cfg(test)] 中,cfg 是配置 configuration 的缩写,它告诉 Rust :当 test 配置项存在时,才运行下面的代码,而 cargo test 在运行时,就会将 test 这个配置项传入进来,因此后面的 tests 模块会被包含进来。

大家看出来了吗?这是典型的条件编译,Cargo 会根据指定的配置来选择是否编译指定的代码,事实上关于条件编译 Rust 能做的不仅仅是这些,在 Cargo 专题中我们会进行更为详细的介绍。

测试私有函数

关于私有函数能否被直接测试,编程社区里一直争论不休,甚至于部分语言可能都不支持对私有函数进行测试或者难以测试。无论你的立场如何,反正 Rust 是支持对私有函数进行测试的:

rust 复制代码
pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

internal_adder 并没有使用 pub 进行声明,因此它是一个私有函数。根据我们之前学过的内容tests 作为另一个模块,是绝对无法对它进行调用的,因为它们根本不在同一个模块中!

但是在上述代码中,我们使用 use super::*;tests 的父模块中的所有内容引入到当前作用域中,这样就可以非常简单的实现对私有函数的测试。

集成测试

与单元测试的同吃同住不同,集成测试的代码是在一个单独的目录下的。由于它们使用跟其它模块一样的方式去调用你想要测试的代码,因此只能调用通过 pub 定义的 API,这一点与单元测试有很大的不同。

如果说单元测试是对代码单元进行测试,那集成测试则是对某一个功能或者接口进行测试,因此单元测试的通过,并不意味着集成测试就能通过:局部上反映不出的问题,在全局上很可能会暴露出来。

tests 目录

一个标准的 Rust 项目,在它的根目录下会有一个 tests 目录,大名鼎鼎的 ripgrep 也不能免俗。

没错,该目录就是用来存放集成测试的,Cargo 会自动来此目录下寻找集成测试文件。我们可以在该目录下创建任何文件,Cargo 会对每个文件都进行自动编译,但友情提示下,最好按照合适的逻辑来组织你的测试代码。

首先来创建一个集成测试文件 tests/integration_test.rs ,注意,tests 目录一般来说需要手动创建,该目录在项目的根目录下,跟 src 目录同级。然后在文件中填入如下测试代码:

rust 复制代码
use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

这段测试代码是对之前私有函数 中的示例进行测试,该示例代码在 src/lib.rs 中。

首先与单元测试有所不同,我们并没有创建测试模块。其次,tests 目录下的每个文件都是一个单独的包,我们需要将待测试的包引入到当前包的作用域后: use adder,才能进行测试 。大家应该还记得包和模块章节中讲过的内容吧?在创建项目后,src/lib.rs 自动创建一个与项目同名的 lib 类型的包,由于我们的项目名是 adder,因此包名也是 adder

因为 tests 目录本身就说明了它的特殊用途,因此我们无需再使用 #[cfg(test)] 来取悦 Cargo。后者会在运行 cargo test 时,对 tests 目录中的每个文件都进行编译运行。

shell 复制代码
$ cargo test
     Running unittests (target/debug/deps/adder-8a400aa2b5212836)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-2d3aeee6f15d1f20)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

运行 cargo test ,可以看到上述输出。测试内容有三个部分:单元测试,集成测试和文档测试。

首先是单元测试被运行 Running unittests ,其次就是我们的主角集成测试的运行 Running tests/integration_test.rs,可以看出,集成测试的输出内容与单元测试并没有大的区别。最后运行的是文档测试 Doc-tests adder

与单元测试类似,我们可以通过指定名称的方式来运行特定的集成测试用例:

shell 复制代码
$ cargo test --test integration_test
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

这次,单元测试、文档测试啥的都没有运行,只有集成测试目录下的 integration_test 文件被顺利执行。

大家可以尝试下在同一个测试文件中添加更多的测试用例或者添加更多的测试文件,并观察测试输出会如何变化。

共享模块

在集成测试的 tests 目录下,每一个文件都是一个独立的包,这种组织方式可以很好的帮助我们理清测试代码的关系,但是如果大家想要在多个文件中共享同一个功能该怎么做?例如函数 setup 可以用于状态初始化,然后多个测试包都需要使用该函数进行状态的初始化。

也许你会想要创建一个 tests/common.rs 文件,然后将 setup 函数放入其中:

rust 复制代码
pub fn setup() {
    // 初始化一些测试状态
    // ...
}

但是当我们运行 cargo test 后,会发现该函数被当作集成测试函数运行了,即使它并没有包含任何测试功能,也没有被其它测试文件所调用:

shell 复制代码
$ cargo test
     Running tests/common.rs (target/debug/deps/common-5c21f4f2c87696fb)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

显然,这个结果并不是我们想要的。为了避免这种输出,我们不能创建 tests/common.rs,而是要创建 tests/common/mod.rs ,此时再运行 cargo test 就不会再看到相应的输出。 原因是通过这种文件组织和命名方式, Rust 不再将 common 模块看作是集成测试文件。

总结来说,tests 目录下的子目录中的文件不会被当作独立的包,也不会有测试输出

rust 复制代码
use adder;

mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

此时,就可以在测试中调用 common 中的共享函数了,不过还有一点值得注意,为了使用 common,这里使用了 mod common 的方式来声明该模块。

二进制包的集成测试

目前来说,Rust 只支持对 lib 类型的包进行集成测试,对于二进制包例如 src/main.rs 是无能为力的。原因在于,我们无法在其它包中使用 use 引入二进制包,而只有 lib 类型的包才能被引入,例如 src/lib.rs

这就是为何我们需要将代码逻辑从 src/main.rs 剥离出去放入 lib 包中,例如很多 Rust 项目中都同时有 src/main.rssrc/lib.rs ,前者中只保留代码的主体脉络部分,而具体的实现通通放在类似后者的 lib 包中。

这样,我们就可以对 lib 包中的具体实现进行集成测试,由于 main.rs 中的主体脉络足够简单,当集成测试通过时,意味着 main.rs 中相应的调用代码也将正常运行。

总结

Rust 提供了单元测试和集成测试两种方式来帮助我们组织测试代码以解决代码正确性问题。

单元测试针对的是具体的代码单元,例如函数,而集成测试往往针对的是一个功能或接口 API,正因为目标上的不同,导致了两者在组织方式上的不同:

  • 单元测试的模块和待测试的代码在同一个文件中,且可以很方便地对私有函数进行测试
  • 集成测试文件放在项目根目录下的 tests 目录中,由于该目录下每个文件都是一个包,我们必须要引入待测试的代码到当前包的作用域中,才能进行测试,正因为此,集成测试只能对声明为 pub 的 API 进行测试

下个章节,我们再来看看该如何使用 GitHub Actions 对 Rust 项目进行持续集成。

推荐几款学习编程的免费平台

免费在线开发平台(https://docs.ltpp.vip/LTPP/

探索编程世界的新天地,为学生和开发者精心打造的编程平台,现已盛大开启!这个平台汇集了近4000道精心设计的编程题目,覆盖了C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#等众多编程语言,为您的编程学习之旅提供了一个全面而丰富的实践环境。

在这里,您不仅可以查看自己的代码记录,还能轻松地在云端保存和运行代码,让编程变得更加便捷。平台还提供了私聊和群聊功能,让您可以与同行们无障碍交流,分享文件,共同进步。不仅如此,您还可以通过阅读文章、参与问答板块和在线商店,进一步拓展您的知识边界。

为了提升您的编程技能,平台还设有每日一题、精选题单以及激动人心的编程竞赛,这些都是备考编程考试的绝佳资源。更令人兴奋的是,您还可以自定义系统UI,选择视频或图片作为背景,打造一个完全个性化的编码环境,让您的编程之旅既有趣又充满挑战。

免费公益服务器(https://docs.ltpp.vip/LTPP-SHARE/linux.html

作为开发者或学生,您是否经常因为搭建和维护编程环境而感到头疼?现在,您不必再为此烦恼,因为一款全新的免费公共服务器已经为您解决了所有问题。这款服务器内置了多种编程语言的编程环境,并且配备了功能强大的在线版VS Code,让您可以随时随地在线编写代码,无需进行任何复杂的配置。
随时随地,云端编码

无论您身在何处,只要有网络连接,就可以通过浏览器访问这款公共服务器,开始您的编程之旅。这种云端编码的便利性,让您的学习或开发工作不再受限于特定的设备或环境。
丰富的编程语言支持

服务器支持包括C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#等在内的多种主流编程语言,满足不同开发者和学生的需求。无论您是初学者还是资深开发者,都能找到适合自己的编程环境。
在线版VS Code,高效开发

内置的在线版VS Code提供了与本地VS Code相似的编辑体验,包括代码高亮、智能提示、代码调试等功能,让您即使在云端也能享受到高效的开发体验。
数据隐私和安全提醒

虽然服务器是免费的,但为了保护您的数据隐私和安全,我们建议您不要上传任何敏感或重要的数据。这款服务器更适合用于学习和实验,而非存储重要信息。

免费公益MYSQL(https://docs.ltpp.vip/LTPP-SHARE/mysql.html

作为一名开发者或学生,数据库环境的搭建和维护往往是一个复杂且耗时的过程。但不用担心,现在有一款免费的MySQL服务器,专为解决您的烦恼而设计,让数据库的使用变得简单而高效。
性能卓越,满足需求

虽然它是免费的,但性能绝不打折。服务器提供了稳定且高效的数据库服务,能够满足大多数开发和学习场景的需求。
在线phpMyAdmin,管理更便捷

内置的在线phpMyAdmin管理面板,提供了一个直观且功能强大的用户界面,让您可以轻松地查看、编辑和管理数据库。
数据隐私提醒,安全第一

正如您所知,这是一项公共资源,因此我们强烈建议不要上传任何敏感或重要的数据。请将此服务器仅用于学习和实验目的,以确保您的数据安全。

免费在线WEB代码编辑器(https://docs.ltpp.vip/LTPP-WEB-IDE/

无论你是开发者还是学生,编程环境的搭建和管理可能会占用你宝贵的时间和精力。现在,有一款强大的免费在线代码编辑器,支持多种编程语言,让您可以随时随地编写和运行代码,提升编程效率,专注于创意和开发。
多语言支持,无缝切换

这款在线代码编辑器支持包括C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#在内的多种编程语言,无论您的项目需要哪种语言,都能在这里找到支持。
在线运行,快速定位问题

您可以在编写代码的同时,即时运行并查看结果,快速定位并解决问题,提高开发效率。
代码高亮与智能提示

编辑器提供代码高亮和智能提示功能,帮助您更快地编写代码,减少错误,提升编码质量。

免费二维码生成器(https://docs.ltpp.vip/LTPP-QRCODE/

二维码(QR Code)是一种二维条码,能够存储更多信息,并且可以通过智能手机等设备快速扫描识别。它广泛应用于各种场景,如:
企业宣传

企业可以通过二维码分享公司网站、产品信息、服务介绍等。
活动推广

活动组织者可以创建二维码,参与者扫描后可以直接访问活动详情、报名链接或获取电子门票。
个人信息分享

个人可以生成包含联系方式、社交媒体链接、个人简历等信息的二维码。
电子商务

商家使用二维码进行商品追踪、促销活动、在线支付等。
教育

教师可以创建二维码,学生扫描后可以直接访问学习资料或在线课程。
交通出行

二维码用于公共交通的票务系统,乘客扫描二维码即可进出站或支付车费。 功能强大的二维码生成器通常具备用户界面友好,操作简单,即使是初学者也能快速上手和生成的二维码可以在各种设备和操作系统上扫描识别的特点。

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习