俗话说:“测试写得好,奖金少不了。”

有经验的开发人员通常会通过单元测试来保证代码基本逻辑的正确性。如果你是一名新手开发者,并且还没体会到单元测试的好处,那么建议你先读一下我之前的一篇文章代码洁癖系列(七):单元测试的地位

写单元测试一般需要三个步骤:

  1. 准备测试用例,测试用例要能覆盖尽可能多的代码
  2. 执行需要测试的代码
  3. 判断结果,是否是你希望得到的结果

了解了这些以后,我们就来看看在Rust中应该怎么写单元测试。

首先我们建立一个library项目

$ cargo new adder --lib
Created library `adder` project

然后在src/lib.rs文件中开始写测试代码

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

此时在命令行运行 cargo test就会得到测试结果

可以看到,结果显示,Rust运行了一项测试并且测试通过。后面的Doc-tests我们先放下,以后再聊。

当然,这并不是我们常见的测试,在日常开发中,我们通常是先写我们的业务代码然后再对各个函数进行单元测试,最后还会对某个模块进行集成测试。那么我们就来模拟一下日常开发过程中应该如何来写测试。

单元测试

我们仍然是用上面的项目,先来在src/lib.rs中写一段“业务代码”

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

这是一段非常简单的代码,对外暴露的函数只是一个加2的功能,内部调用了一个两数相加的函数。现在我们就对这个内部函数做一个单元测试。

#[cfg(test)]
mod tests {
use super::*; #[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}

在测试模块中,如果想要使用我们业务代码中的函数,就需要通过 use super::*;将其引入可用范围。接着,还是执行 cargo test,测试结果与刚才类似。

测了半天全是通过的没什么意思,单元测试真正的作用是要发现代码中的问题,所以我们来尝试一个错误的试一下。假设我们希望2+2等于5。

这里我们的assert_eq!左右不相等,引起了线程恐慌,因此导致测试失败。结果中给出了失败的原因,引起失败的位置,并且有一句提示: note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. 我们按照这个提示,设置变量RUST_BACKTRACE=1,此时再执行cargo test

Rust就会将错误栈打印出来,根据结果提示,这并不是完整的错误栈,我们还可以将RUST_BACKTRACE设置为full来查看更加详细的信息。这里我就不做演示了。

集成测试

接下来我们再演示一下集成测试。我们通常将集成测试单独放到一个目录中,在lib.rs文件中,rust识别测试mod的名称是tests,同样的,我们在src下创建tests目录。tests目录下就是我们的所有集成测试代码。

如图,integration_test是我们测试代码的文件,common目录下的mod.rs文件中是一些集成测试必要的配置。这里我们只是放了一个空的setup函数。

在集成测试中,我们就要像正常他人使用我们的代码时那样来进行测试,首先需要将我们的mod引入到可用范围,当然还需要加上common的mod。

use adder;

mod common;

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

接着就可以测试我们对外暴露的函数了。

ok,集成测试的方法我们也掌握了。现在来看看一直被我们忽略的Doc-tests吧。

文档测试

我们已经知道,Rust中的注释是双斜线//,像我们刚刚写的library代码,如果想要把它发布到crate.io上让别人使用,那么我们就需要增加相应的文档,这里文档的每行都应该是三斜线///开头,而文档中也应该放一些例子供他人参考。

/// Adds two to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = adder::add_two(arg);
///
/// assert_eq!(7, answer);
/// ```
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}

现在我给add_two函数加上了文档,我们再次执行 cargo test命令。

现在我们就明白了,Doc-tests测试就是运行我们文档中的例子。

常用特性

到目前为止,我们已经知道了在Rust中如何写测试代码了。接下来我们再来了解几个比较常用的特性。

运行指定的测试代码

我们在开发过程中肯定不会每次都去跑全量的单元测试,那样太浪费时间了。通常是我们开发完一个功能之后,编写对应的单元测试,然后单独跑这个测试。那么Rust中能不能单独跑一个单元测试呢?答案是肯定的。

相信细心的同学已经发现了,Rust测试结果中,是针对每个测试单独统计结果,并且每个测试都有自己的名字,像我们前面写的it_worksinternal。假设我们的代码中同时存在这两个函数,如果你想要单独跑internal这一个测试,就可以使用cargo test internal命令。

你也可以使用这种方法来执行多个名称类似的测试,假如我们有名称为internal_a的测试,那么执行cargo test internal命令时它也会被执行。

忽略某个测试

当我们有一个测试执行时间非常长的时候,我们一般不会轻易去执行,这时如果你想要执行多个测试,除了用我们上面提到的方法,去指定不同的名称列表以外。还可以把这个测试忽略掉。

现在我不想执行internal测试了,只需要对代码进行如下改动:

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

这时再来运行测试,结果如图所示。

我们发现此时internal测试已经被忽略了。

测试异常情况

除了测试代码逻辑正常的情况,我们有时还需要测试一些异常情况,比如接收到非法参数时程序能否返回我们希望看到的异常。

我们首先来看一下如何测试程序返回异常信息。

Rust为我们提供了一个叫做should_panic的注解。我们可以使用它来测试程序是否返回异常:

pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
} fn internal_adder(a: i32, b: i32) -> i32 {
if a < 0 {
panic!("a should bigger than 0");
}
a + b
} #[cfg(test)]
mod tests {
use super::*; #[test]
#[should_panic]
fn internal() {
assert_eq!(4, internal_adder(-2, 2));
}
}

此时我们运行测试时就会发现internal测试通过,因为它发生了线程恐慌,这是我们希望看到的结果。

另外,我们还可以再指定我们具体期望的异常,那么就可以在should_panic后面加上expected参数。

#[test]
#[should_panic(expected = "a should be positive")]
fn internal() {
assert_eq!(4, internal_adder(-2, 2));
}

大家可以自行运行一下这段测试代码看看效果。

总结

文中我向大家介绍了在Rust中如何进行单元测试、集成测试,还有比较特殊的文档测试。最后还介绍了3种常见的测试特性。

最后想友情提醒大家一下,在开发过程中,不要写完一堆功能后再开始写单元测试,这时你很有可能会因为测试代码过于繁琐而放弃。建议大家每写一个功能,随即开始进行单元测试,这样也能立即看到自己的代码的执行效果,提高成就感。这就是所谓的“步步为营”。

Rust入坑指南:步步为营的更多相关文章

  1. Rust入坑指南:核心概念

    如果说前面的坑我们一直在用小铲子挖的话,那么今天的坑就是用挖掘机挖的. 今天要介绍的是Rust的一个核心概念:Ownership.全文将分为什么是Ownership以及Ownership的传递类型两部 ...

  2. Rust入坑指南:鳞次栉比

    很久没有挖Rust的坑啦,今天来挖一些排列整齐的坑.没错,就是要介绍一些集合类型的数据类型."鳞次栉比"这个标题是不是显得很有文化? 在Rust入坑指南:常规套路一文中我们已经介绍 ...

  3. Rust入坑指南:亡羊补牢

    如果你已经开始学习Rust,相信你已经体会过Rust编译器的强大.它可以帮助你避免程序中的大部分错误,但是编译器也不是万能的,如果程序写的不恰当,还是会发生错误,让程序崩溃.所以今天我们就来聊一聊Ru ...

  4. Rust入坑指南:朝生暮死

    今天想和大家一起把我们之前挖的坑再刨深一些.在Java中,一个对象能存活多久全靠JVM来决定,程序员并不需要去关心对象的生命周期,但是在Rust中就大不相同,一个对象从生到死我们都需要掌握的很清楚. ...

  5. Rust入坑指南:齐头并进(上)

    我们知道,如今CPU的计算能力已经非常强大,其速度比内存要高出许多个数量级.为了充分利用CPU资源,多数编程语言都提供了并发编程的能力,Rust也不例外. 聊到并发,就离不开多进程和多线程这两个概念. ...

  6. Rust入坑指南:常规套路

    搭建好了开发环境之后,就算是正式跳进Rust的坑了,今天我就要开始继续向下挖了. 由于我们初来乍到 ,对Rust还不熟悉,所以我决定先走一遍常规套路. 变不变的变量 学习一门语言第一个要了解的当然就是 ...

  7. Rust入坑指南:坑主驾到

    欢迎大家和我一起入坑Rust,以后我就是坑主,我主要负责在前面挖坑,各位可以在上面看,有手痒的也可以和我一起挖.这个坑到底有多深?我也不知道,我是抱着有多深就挖多深的心态来的,下面我先跳了,各位请随意 ...

  8. Rust入坑指南:千人千构

    坑越来越深了,在坑里的同学让我看到你们的双手! 前面我们聊过了Rust最基本的几种数据类型.不知道你还记不记得,如果不记得可以先复习一下.上一个坑挖好以后,有同学私信我说坑太深了,下来的时候差点崴了脚 ...

  9. Rust入坑指南:有条不紊

    随着我们的坑越来越多,越来越大,我们必须要对各种坑进行管理了.Rust为我们提供了一套坑务管理系统,方便大家有条不紊的寻找.管理.填埋自己的各种坑. Rust提供给我们一些管理代码的特性: Packa ...

随机推荐

  1. 【转】分布式服务框架 Zookeeper -- 管理分布式环境中的数据

    Zookeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务.状态同步服务.集群管理.分布式应用配置项的管理 ...

  2. LR Java脚本编写方法

    之前在某一家银行也接触过java写的性能接口脚本,最近因项目,也需编写java接口性能测试脚本,脑袋一下懵逼了,有点不知道从何入手.随后上网查了相关资料,自己又稍微总结了一下,与大家共同分享哈~ 首先 ...

  3. python报错: invalid syntax

    invalid syntax: 无效的语法. 解决办法:查看当前语句中的  , 如果当前行没找到错误,依次往上找,往上找时可以利用是否有输出进行快速查找. 原因:python语法很严格,少了左括号.右 ...

  4. RocketMQ客户端加载流程

     这节介绍RocketMQ客户端的启动流程,即Consumer和Producer的启动流程. 1. 客户端demo  首先先看下客户端的demo Producer: public class Sync ...

  5. Java 用链表实现栈和队列

    栈 是一种基于后进先出(LIFO)策略的集合类型.当邮件在桌上放成一叠时,就能用栈来表示.新邮件会放在最上面,当你要看邮件时,会一封一封从上到下阅读.栈的顶部称为栈顶,所有操作都在栈顶完成. 前面提到 ...

  6. dfs - 走过的标记取消

    在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别.要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C. ...

  7. 蒙蔽的FormBody

    作为一个不算新人的新人,今天看到 了FormBody这个绿色字体,之前没有怎么注意过, 好了 ,发现了一篇文章,记录下. 这篇文章总结下来就是: 在前端穿过的数据是Json格式(当我们设置Conten ...

  8. Python解析json字符串,json字符串用法

    json数据简介 json数据是一个轻量级的数据交换格式,采用完全独立于语言的文本格式,这些特性使json称为理想的数据交换语言,易于人阅读和编写,同时易于机器解析和生成. json中的字符集必须是U ...

  9. Scheme实现数字电路仿真(2)——原语

    版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖.如要转贴,必须注明原文网址 http://www.cnblogs.com/Colin-Cai/p/12045295.html 作者:窗户 ...

  10. 04-String

    动手动脑 请运行以下示例代码StringPool.java,查看其输出结果.如何解释这样的输出结果?从中你能总结出什么? s0,s1,s2字符串的内容相同,实际占用的是同一空间,引用的是同一个对象,所 ...