测试是软件工程的一个组成部分。对于初学者来说,写一个测试用例可以确保你的代码完全按照你的期望来做。每种编程语言都有各种框架,帮助你测试你的代码。
小的宠物项目可以不进行测试,但随着应用程序的扩展,你会遇到撞墙的风险,在你把一个新功能推到生产中后,你会变得很偏执。
一些团队使用手动测试员来进行回归测试。这在理论上是很好的,但手动测试员不能捕捉到在运行时出现的所有错综复杂的问题。而且,考虑到可用于自动化测试的工具,使用手动测试员是昂贵的,也是无益的。
尽管如此,喜欢测试他们的代码的工程师的比例非常小;但如果你看看那些构建高质量软件的最好的工程团队,测试将是他们工作流程中不可或缺的一部分。
在这篇文章中,我们将深入探讨嘲弄Rust。我们将了解嘲讽与一般的单元测试有什么不同,以及如何使用Mockall库实现嘲讽。
本文假设你对Rust有一定的熟悉程度。如果你是第一次接触Rust,你可以在这里阅读Rust的介绍。
什么是单元测试?
现在你知道了测试你的代码的重要性,让我们看看单元测试是如何工作的。当你理解了单元测试的工作原理后,你就会明白嘲弄的必要性。
让我们假设你有一个函数,接收两个数字并返回这两个数字的除数。
function divide (a,b){
return a / b
}
很简单的函数。你提供两个数字,然后你得到一个输出;但是,问题是,情况总是这样吗?
如果b 是0呢?这在大多数语言中会产生零除法错误,因为任何东西除以0都是无穷大。
如果a 和b 是数组呢?你能确定调用你的函数的代码将只传递预期的数据类型吗?不幸的是,你无法做到。
这就是单元测试的作用。单元测试以多种方式测试你的代码,以确保你的代码能够处理这些类型的异常情况。
嗯,它不会自动做到这一点--你必须自己编写这些测试案例。
例如,为了测试除法函数,你要写一些测试用例,如下图所示。
expect divide(2,2) to be 1
expect divide (1,0) to throw an error
现在你知道为什么开发人员通常不喜欢写测试用例了。这是一个很大的工作,但一旦你习惯了它,它的好处就证明了它的努力。
什么是Mocking?
那么,单元测试听起来很简单;现在,到底什么是嘲弄?
除法函数的例子是一个简单的例子。现实世界中的函数可能相当复杂。而且,函数可能有很多依赖关系。
例如,一个将.pdf转换为文本的函数可能会使用一个外部依赖,如pdf-extract。为了测试这些复杂的函数,你将不得不在你的测试用例中包括所有的外部依赖关系。
这就使事情变得太复杂了。如果有一个更简单的方法呢?有的,它叫做 "模拟"。
一个被测试的对象/函数可能与其他对象有依赖关系。为了隔离你试图测试的函数的行为,你可以通过使用mock来替代对象。简单地说,"模拟 "可以模拟复杂函数的行为,而不需要包括它们。
当真正的函数/对象难以包含在测试案例中时,这通常是有用的。例如,为了测试一些数据库操作,你可以使用一个简单的内存存储来读写数据。
这将模拟数据库的操作,而不需要建立一个实际的数据库来测试其功能。这就是嘲弄背后的核心思想。
这里有一篇关于嘲弄如何工作的优秀文章。现在,让我们来看看Rust是如何帮助我们进行嘲讽的。
Rust中的嘲讽方法
Rust有各种各样的嘲讽库,它们以不同的方式工作。每个嘲讽库都有自己的一套功能、优点和缺点。在这篇文章中,我们将看一下Mockall库。
Mockall
虽然有很多Rust的嘲讽库,但Mockall是目前最强大的嘲讽库。它结合了许多其他嘲讽库的优点。它也有一个用户友好的界面。它不使用任何不安全的代码,并在稳定的Rust上运行。此外,Mockall包含了开发结构或特征的模拟版本的工具。
Mockall是如何工作的?
在我们深入编写模拟程序之前,你必须了解结构体和特质之间的区别。简单地说,结构体和特质类似于大多数语言中的类和属性。这里有一篇很好的文章,可以了解更多关于结构体、特质和植入块的知识。
以下是使用Mockall的两种主要方法。
- #[automock],用于模拟只有植入块的特质或结构
- mock!, 用来模拟第一种方法中没有涉及到的其他东西。
让我们来写一些代码。首先,让我们写一个简单的例子来模拟一个trait,并期望它返回我们正在寻找的东西。
#[automock]
trait MyNewTrait {
fn func1(&self) -> u32;
fn func2(&self, x: u32, y: u32) -> u32;
}
let mut mock = MockMyNewTrait::new();
mock.expect_func1()
.return_const(42u32);
mock.expect_func2()
.returning(|x, y| x + y);
上面的例子展示了我们如何使用#[automock]定义来模拟一个简单的特质。
现在,让我们来模拟一个结构。由于#[automock]只适用于只有一个植入块的结构,所以我们必须使用!!mock定义。我们将编写一个实现多个特征的模拟结构。
pub trait Trait1 {
fn func1(&self);
}
pub trait Trait2: Trait1 {
fn func2(&self);
}
mock! {
Trait3 {}
impl Trait1 for Trait3 {
fn func1(&self);
}
impl Trait2 for Trait3 {
fn func2(&self);
}
}
let mut mock = MockTrait3::new();
mock.expect_func1().returning(|| ());
mock.func2().returning(|| ());
mock.func1();
mock.func2();
在上面的例子中,我们有两个traits,后面是一个实现这两个traits的mock结构。这使用了 !mock 宏,因为有不止一个实现。
这就是使用Mockall的两种方式。除了嘲弄结构和特征,你还可以使用Mockall库嘲弄整个模块。
Rust中嘲讽的替代品
除了Mockall之外,Rust中还有其他的嘲讽方式。让我们简单了解一下其中的几个。
Mockers
Mockers是Rust中最古老的嘲讽库,其灵感来源于GoogleMock。Mockers有大量的辅助方法,以及高效的语法,现在仍然在稳定的Rust上使用。然而,需要夜间支持来处理通用函数。
Mock Derive
Mock Derive是第一个引入了从目标特征中 "派生 "模拟对象的概念的模拟库。它在简化整个嘲讽过程中非常有用。
Mock Derive以其用户友好性而闻名。然而,它的主要缺点是它不能验证方法参数。此外,你不能将Mock Derive用于泛型特质、具有泛型方法的特质或多个特质。与其他可用的库相比,它也没有被积极开发。
Galvanic mock
Galvanic-mock是测试库的一部分,与galvanic-test和galvanic-asset一起工作。它为Rust程序提供了全面的测试功能。
Galvanic-mock的突出特点是,它区分了mock的实际功能和mock的预期使用方式。
伪装
需要夜间编译器的嘲讽库提供了诸如调整语言语法的能力的好处。然而,这样的库在本质上是不稳定的。不能保证夜间运行的代码也能在未来的编译器中运行。
Pseudo消除了这个问题。它从确保它能在稳定的Rust上工作中删除了像派生这样的依赖夜间的特性。值得注意的是,Pseudo的理解和使用相当复杂,特别是对初学者来说。
Mock-it
Mock-it是Rust中另一个可用的嘲讽库。由于其简单性,它正在变得流行。使用Mock-it的主要好处是,它可以在稳定的Rust上运行,而Rust中的大多数嘲讽库都是使用代码生成的实验性的。缺点是,Mock-it没有一个高级的API。因此,它在大规模嘲讽中的实际应用是非常有限的。
Mocktopus
Mocktopus从一长串的Rust的嘲讽库中脱颖而出。它专注于自由函数而不是专注于嘲讽特性。使用Mocktopus有三个主要好处。
首先,它有一个低门槛的技能。第二,它可以处理通用函数。第三,它可以同时嘲弄结构体和自由函数。
总结
嘲讽是软件测试的一个核心组成部分。除了单元测试外,嘲弄允许你模拟复杂组件的行为,同时保持你的测试用例更简单的编写和执行。Mockall是一个非常棒的库,它提供了Rust中嘲弄的所有必要工具。