单元测试应该是小型测试(原子的)、轻量级的和快速的。但是,被测对象可能依赖于其他对象。它可能需要与数据库交互,与邮件服务器通信,或者与 Web 服务或消息队列通信。在单元测试期间,所有这些服务可能都不可用。即使它们可用, 对被测对象 及其依赖项进行单元测试也会花费不可接受的时间。如果什么...
- Web 服务无法访问?
- 数据库停机维护?
- 消息队列又重又慢?
这些都破坏了单元测试的原子性、轻量级和快速的全部目的。我们希望单元测试在几毫秒内执行。如果单元测试很慢,你的构建就会变慢,这会影响你的开发团队的生产力。解决方案是使用模拟,这是一种为被测试的类提供测试替身的方法。
如果您一直遵循 面向对象编程的 SOLID 原则 ,并使用 Spring 框架进行 依赖注入 ,模拟将成为单元测试的自然解决方案。你真的不需要数据库连接。您只需要一个返回预期结果的对象。如果您编写了紧密耦合的代码,那么您将很难使用模拟。我见过很多无法进行单元测试的遗留代码,因为它们与其他依赖对象紧密耦合。这个不可测试的代码没有遵循面向对象编程的 SOLID 原则,也没有使用依赖注入。
模拟对象:介绍
在单元测试中,测试替身是被测对象的依赖组件(协作者)的替代品。测试替身提供与协作者相同的界面。它可能不是完整的接口,而是用于测试所需的功能。此外,测试替身不必完全像合作者那样行事。目的是模仿协作者,让被测对象认为它实际上是在使用协作者。
根据在测试中扮演的角色,可以有不同类型的测试替身,模拟对象就是其中之一。其他一些类型是虚拟对象、假对象和存根。
模拟对象与其他对象的不同之处在于它使用行为验证。这意味着模拟对象验证 它(模拟对象)是否被被测对象正确使用 。如果验证成功,则可以认为被测对象正确使用了真正的协作者。
测试场景
对于测试场景,请考虑产品订购服务。客户端与 DAO 交互以完成产品订购流程。
我们将从
Product
域对象和 DAO 接口
ProductDao
开始。
产品.java
package guru.springframework.unittest.mockito;
public class Product {
}
ProductDao.java
package guru.springframework.unittest.mockito;
public class Product {
}
出于示例的目的,我将
Product
类保留为空。但在实际应用程序中,它通常是一个实体,其状态具有相应的 getter 和 setter 方法,以及任何已实现的行为。
在
ProductDao
接口中,我们声明了两个方法:
-
getAvailableProducts()
方法返回传递给它的Product
的可用数量。 -
orderProduct()
为产品下订单。
我们接下来要写的
ProductService
类是我们感兴趣的
——
被测对象
。
产品服务.java
package guru.springframework.unittest.mockito;
public class Product {
}
上面的
ProductService
类由
ProductDao
组成,通过 setter 方法进行初始化。在
buy()
方法中,我们调用了
ProductDao
的
getAvailableProducts()
来检查是否有足够数量的指定产品可用。如果不是,则抛出
InsufficientProductsException
类型的异常。如果有足够的数量可用,我们调用
ProductDao
的
orderProduct()
方法。
我们现在需要的是对 ProductService 进行单元测试。但如您所见, ProductService 由 ProductDao 组成,我们还没有其实现。它可以是从远程数据库检索数据的 Spring Data JPA 实现,或者是与托管基于云的存储库的 Web 服务通信的实现——我们不知道。即使我们有一个实现,我们也会在稍后的集成测试中使用它,这是我之前写的一种 软件测试 类型。但是现在,我们 对这个单元测试中的任何外部实现都不感兴趣 。
在单元测试中,我们不应该为实现正在做什么而烦恼。我们想要的是测试我们的 ProductService 是否按预期运行,以及它是否能够正确使用其协作者。为此,我们将使用 Mockito 模拟 ProductDao 和 Product 。
ProductService 类还抛出自定义异常 InsufficientProductsException 。异常类的代码是这样的。
InsufficientProductsException.java
package guru.springframework.unittest.mockito;
public class Product {
}
使用 Mockito
Mockito 是一个用 Java 编写的用于单元测试的模拟框架。它是 github 上可用的开源框架。您可以将 Mockito 与 JUnit 结合使用,以在单元测试期间创建和使用模拟对象。要开始使用 Mockito, 请下载 JAR 文件并将其放入您的项目类中。如果你使用 Maven,你需要在 pom.xml 文件中添加它的依赖,如下所示。
pom.xml
package guru.springframework.unittest.mockito;
public class Product {
}
一旦设置了所需的依赖项,就可以开始使用 Mockito。但是,在我们开始使用模拟进行任何单元测试之前,让我们快速概述一下关键的模拟概念。
模拟对象创建
对于我们的示例,很明显我们需要模拟
ProductDao
和
Product
。最简单的方法是调用
Mockito
类的
mock()
方法。 Mockito 的好处在于它允许创建接口和类的模拟对象,而无需强制任何显式声明。
MockCreationTest.java
package guru.springframework.unittest.mockito;
public class Product {
}
另一种方法是使用
@Mock
注释。使用它时,您需要通过调用
MockitoAnnotations.initMocks(this)
来初始化模拟,或者将
MockitoJUnitRunner
指定为 JUnit 测试运行程序
@RunWith(MockitoJUnitRunner.class)
。
MockCreationAnnotationTest.java
package guru.springframework.unittest.mockito;
public class Product {
}
存根
Stubbing 意味着模拟模拟对象方法的行为。我们可以通过对方法调用设置期望来在模拟对象上存根方法。例如,我们可以存根
ProductDao
模拟的
getAvailableProducts()
方法以在调用该方法时返回特定值。
package guru.springframework.unittest.mockito;
public class Product {
}
在上面代码的
第 4 行
中,我们将
ProductDao
的
getAvailableProducts(product)
存根以返回
30
。
when()
方法表示启动存根的触发器,而
thenReturn()
表示触发器的操作——在示例代码中是返回值
30
。在带有
断言
的
第 5 行
中,我们确认存根按预期执行。
验证
我们的目标是测试 ProductService ,现在我们只模拟 Product 和 ProductDao 并存根 ProductDao 的 getAvailableProducts() 。
我们现在要验证
ProductService
的
buy()
方法的行为。首先,我们要验证它是否使用所需的参数集调用
ProductDao
的
orderProduct()
。
package guru.springframework.unittest.mockito;
public class Product {
}
在
第 6 行
中,我们调用了被测
ProductService
的
buy()
方法。在
第 7 行
中,我们验证了
ProductDao
模拟 get 的
orderProduct()
方法是使用预期的参数集(我们传递给
buy()
)调用的。
我们的测试通过了。但是,还没有完成。我们还想验证:
- 方法调用次数 : buy() 方法至少调用一次 getAvailableProduct() 。
- 调用顺序 : buy() 方法首先调用 getAvailableProduct() ,然后调用 orderProduct() 。
- 异常验证 :如果传递给 buy() 方法的订单数量多于 getAvailableProduct() 返回的可用数量,则 buy() 方法失败并出现 InsufficientProductsException 。
- 异常期间的行为 :抛出 InsufficientProductsException 时, buy() 方法不调用 orderProduct() 。
这是完整的测试代码。
ProductServiceTest.java
package guru.springframework.unittest.mockito;
public class Product {
}
我已经在上面解释了测试类的初始代码。因此,我们将从
第 36 行到第 38 行
开始,我们在其中使用
inOrder()
方法来验证
buy()
方法对
ProductDao
进行的方法调用顺序。
然后,我们编写了一个
purchaseWithInsufficientAvailableQuantity()
测试方法,以检查当订单数量超过可用数量时是否按预期抛出
InsufficientProductsException
。我们还在
第 54
行验证了如果抛出
InsufficientProductsException
,则不会调用
orderProduct()
方法。
测试的输出是这样的。
package guru.springframework.unittest.mockito;
public class Product {
}
概括
单元测试中的模拟广泛用于 Spring 的企业应用程序开发 中。通过使用 Mockito,您可以将要测试的类中的 @Autowired 组件替换为模拟对象。您将通过注入模拟服务成为单元测试控制器。您还将设置服务以使用模拟 DAO 对服务层进行单元测试。要对 DAO 层进行单元测试,您将模拟数据库 API。这个列表是无穷无尽的——这取决于您正在处理的应用程序类型和被测对象。如果您遵循 依赖倒置原则 并使用 依赖注入 ,模拟就会变得容易。
Mockito 库是一个非常庞大且成熟的模拟库。在单元测试中用于模拟对象非常流行。 Mockito 很受欢迎,因为它易于使用,而且用途广泛。我写这篇文章只是为了介绍模拟和 Mockito。查看 官方 Mockito 文档 以了解 Mockito 的所有功能。