单元测试中的模拟

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

单元测试应该是小型测试(原子的)、轻量级的和快速的。但是,被测对象可能依赖于其他对象。它可能需要与数据库交互,与邮件服务器通信,或者与 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 的所有功能。