Spring 单元测试中如何进行 mock的实现

我们在使用 Spring 开发项目时,都会用到依赖注入。如果程序依赖了外部系统或者不可控组件,比如依赖数据库、网络通信、文件系统等,我们在编写单元测试时,并不需要实际对外部系统进行操作,这时就要将被测试代码与外部系统进行解耦,而这种解耦方法就叫作 “mock”。所谓 “mock” 就是用一个“假”的服务代替真正的服务。

那我们如何来 mock 服务进行单元测试呢?mock 的方式主要有两种:手动 mock 和利用单元测试框架 mock。其中,利用框架 mock 主要是为了简化代码编写。我们这里主要是介绍利用框架 mock,而手动 mock 只是简单介绍。

手动 mock

手动 mock 其实就是重新创建一个类继承被 mock 的服务类,并重写里面的方法。在单元测试中,利用依赖注入的方式使用 mock 的服务类替换原来的服务类。具体代码示列如下:

/**
 * UserRepository
 *
 * @author star
 */
@Repository
public class UserRepository {

  /**
   * 模拟从数据库中获取用户信息,实际开发中需要连接真实的数据库
   */
  public User getUser(String name) {
    User user = new User();
    user.setName("testing");
    user.setEmail("testing@outlook.com");

    return user;
  }
}

/**
 * MockUserRepository
 *
 * @author star
 */
public class MockUserRepository extends UserRepository {

  /**
   * 模拟从数据库中获取用户信息
   */
  @Override
  public User getUser(String name) {
    User user = new User();
    user.setName("mock-test-name");
    user.setEmail("mock-test-email");

    return user;
  }
}

// 进行单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceManualTest {

  @Autowired
  private UserService userService;

  @Test
  public void testGetUser_Manual() {
    // 将 MockUserRepository 注入到 UserService 中
    userService.setUserRepository(new MockUserRepository());
    User user = userService.getUser("mock-test-name");
    Assert.assertEquals("mock-test-name", user.getName());
    Assert.assertEquals("mock-test-email", user.getEmail());
  }
}

从上面的代码中,我们可以看到手动 mock 需要编写大量的额外代码,同时被测试类也需要提供依赖注入的入口(setter 方法等)。如果被 mock 的类修改了函数名称或者功能,mock 类也要跟着修改,增加了维护成本。

为了提高效率,减少维护成本,我们推荐使用单元测是框架进行 mock。

利用框架 mock

这里我们主要介绍 Mokito.mock()、@Mock、@MockBean 这三种方式的 mock。

Mocito.mock()

Mocito.mock() 方法允许我们创建类或接口的 mock 对象。然后,我们可以使用 mock 对象指定其方法的返回值,并验证其方法是否被调用。代码示列如下:

@Test
public void testGetUser_MockMethod() {
  // 模拟 UserRepository,测试时不直接操作数据库
  UserRepository mockUserRepository = Mockito.mock(UserRepository.class);
  // 将 mockUserRepository 注入到 UserService 类中
  userService.setUserRepository(mockUserRepository);

  User mockUser = mockUser();
  Mockito.when(mockUserRepository.getUser(mockUser.getName()))
      .thenReturn(mockUser);

  User user = userService.getUser(mockUser.getName());
  Assert.assertEquals(mockUser.getName(), user.getName());
  Assert.assertEquals(mockUser.getEmail(), user.getEmail());

  // 验证 mockUserRepository.getUser() 方法是否执行
  Mockito.verify(mockUserRepository).getUser(mockUser.getName());
}

@Mock

@Mock 是 Mockito.mock() 方法的简写。同样,我们应该只在测试类中使用它。与 Mockito.mock() 方法不同的是,我们需要在测试期间启用 Mockito 注解才能使用 @Mock 注解。

我们可以调用 MockitoAnnotations.initMocks(this) 静态方法来启用 Mockito 注解。为了避免测试之间的副作用,建议在每次测试执行之前先进行以下操作:

@Before
public void setup() {
  // 启用 Mockito 注解
  MockitoAnnotations.initMocks(this);
}

我们还可以使用另一种方法来启用 Mockito 注解。通过在 @RunWith() 指定 MockitoJUnitRunner 来运行测试:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceMockTest { 

}

下面我们来看看如何使用 @Mock 进行服务 mock。代码示列如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceMockTest {

  @Mock
  private UserRepository userRepository;

  @Autowired
  @InjectMocks
  private UserService userService;

  private User mockUser() {
    User user = new User();
    user.setName("mock-test-name");
    user.setEmail("mock-test-email");

    return user;
  }

  @Before
  public void setup() {
    // 启用 Mockito 注解
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void testGetUser_MockAnnotation() {
    User mockUser = mockUser();
    Mockito.when(userRepository.getUser(mockUser.getName()))
        .thenReturn(mockUser);

    User user = userService.getUser(mockUser.getName());
    Assert.assertEquals(mockUser.getName(), user.getName());
    Assert.assertEquals(mockUser.getEmail(), user.getEmail());

    // 验证 mockUserRepository.getUser() 方法是否执行
    Mockito.verify(userRepository).getUser(mockUser.getName());
  }

}

Mockito 的 @InjectMocks 注解作用是将 @Mock 所修饰的 mock 对象注入到指定类中替换原有的对象。

@MockBean

@MockBean 是 Spring Boot 中的注解。我们可以使用 @MockBean 将 mock 对象添加到 Spring 应用程序上下文中。该 mock 对象将替换应用程序上下文中任何现有的相同类型的 bean。如果应用程序上下文中没有相同类型的 bean,它将使用 mock 的对象作为 bean 添加到上下文中。

@MockBean 在需要 mock 特定 bean(例如外部服务)的集成测试中很有用。

要使用 @MockBean 注解,我们必须在 @RunWith() 中指定 SpringRunner 来运行测试。代码示列如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceMockBeanTest {

  @MockBean
  private UserRepository userRepository;

  private User mockUser() {
    User user = new User();
    user.setName("mock-test-name");
    user.setEmail("mock-test-email");

    return user;
  }

  @Test
  public void testGetUser_MockBean() {
    User mockUser = mockUser();
    // 模拟 UserRepository
    Mockito.when(userRepository.getUser(mockUser.getName()))
        .thenReturn(mockUser);
    // 验证结果
    User user = userRepository.getUser(mockUser.getName());
    Assert.assertEquals(mockUser.getName(), user.getName());
    Assert.assertEquals(mockUser.getEmail(), user.getEmail());

    Mockito.verify(userRepository).getUser(mockUser.getName());
  }
}

这里需要注意的是,Spring test 默认会重用 bean。如果 A 测试使用 mock 对象进行测试,而 B 测试使用原有的相同类型对象进行测试,B 测试在 A 测试之后运行,那么 B 测试拿到的对象是 mock 的对象。一般这种情况是不期望的,所以需要用 @DirtiesContext 修饰上面的测试避免这个问题。

最后,小伙伴们可以在 GitHub 中获取源码

到此这篇关于Spring 单元测试中如何进行 mock的实现的文章就介绍到这了,更多相关Spring 单元测试mock内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot MockMvc单元测试的示例代码

    为何使用MockMvc? 对模块进行集成测试时,希望能够通过输入URL对Controller进行测试,如果通过启动服务器,建立http client进行测试,这样会使得测试变得很麻烦,比如,启动速度慢,测试验证不方便,依赖网络环境等,所以为了可以对Controller进行测试,我们引入了MockMVC. MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快.不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而

  • Spring Boot单元测试中使用mockito框架mock掉整个RedisTemplate的示例

    概述 当我们使用单元测试来验证应用程序代码时,如果代码中需要访问Redis,那么为了保证单元测试不依赖Redis,需要将整个Redis mock掉.在Spring Boot中结合mockito很容易做到这一点,如下代码: import org.mockito.Mockito; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration

  • 基于Springboot+Junit+Mockito做单元测试的示例

    前言 这篇文章介绍如何使用Springboot+Junit+Mockito做单元测试,案例选取撮合交易的一个类来做单元测试. 单元测试前先理解需求 要写出好的单测,必须先理解了需求,只有知道做什么才能知道怎么测.但本文主要讲mockito的用法,无需关注具体需求.所以本节略去具体的需求描述. 隔离外部依赖 Case1. 被测类中被@Autowired 或 @Resource 注解标注的依赖对象,如何控制其返回值 以被测方法 MatchingServiceImpl.java的matching(Ma

  • Spring 单元测试中如何进行 mock的实现

    我们在使用 Spring 开发项目时,都会用到依赖注入.如果程序依赖了外部系统或者不可控组件,比如依赖数据库.网络通信.文件系统等,我们在编写单元测试时,并不需要实际对外部系统进行操作,这时就要将被测试代码与外部系统进行解耦,而这种解耦方法就叫作 "mock".所谓 "mock" 就是用一个"假"的服务代替真正的服务. 那我们如何来 mock 服务进行单元测试呢?mock 的方式主要有两种:手动 mock 和利用单元测试框架 mock.其中,利用

  • Spring项目中使用Junit单元测试并配置数据源的操作

    目录 Spring 使用Junit单元测试并配置数据源 一.问题描述 二.解决方案 Spring 数据库依赖 单元测试的一点想法 一点想法: 这里面有这样一些问题: Spring 使用Junit单元测试并配置数据源 一.问题描述 由于公司项目中的数据源是配置在Tomcat中的server.xml中的,所以在使用Junit进行单元测试的时候,无法获取数据源. 二.解决方案 由于项目集成了Spring的自动注入等功能,所以在使用Junit进行单元测试的时候需要保证Spring的配置文件都能被加载,同

  • Spring MVC中的Controller进行单元测试的实现

    目录 导入静态工具方法 初始化MockMvc 执行测试 测试GET接口 测试POST接口 测试文件上传 定义预期结果 写在最后 对Controller进行单元测试是Spring框架原生就支持的能力,它可以模拟HTTP客户端发起对服务地址的请求,可以不用借助于诸如Postman这样的外部工具就能完成对接口的测试.具体来讲,是由Spring框架中的spring-test模块提供的实现,详见MockMvc. 如下将详细阐述如何使用MockMvc测试框架实现对“Spring Controller”进行单

  • Spring Boot中使用Spring-data-jpa实现数据库增删查改

    在实际开发过程中,对数据库的操作无非就"增删改查".就最为普遍的单表操作而言,除了表和字段不同外,语句都是类似的,开发人员需要写大量类似而枯燥的语句来完成业务逻辑. 为了解决这些大量枯燥的数据操作语句,我们第一个想到的是使用ORM框架,比如:Hibernate.通过整合Hibernate之后,我们以操作Java实体的方式最终将数据改变映射到数据库表中. 为了解决抽象各个Java实体基本的"增删改查"操作,我们通常会以泛型的方式封装一个模板Dao来进行抽象简化,但是这

  • 详解spring boot中使用JdbcTemplate

    本文将介绍如何将spring boot 与 JdbcTemplate一起工作. Spring对数据库的操作在jdbc上面做了深层次的封装,使用spring的注入功能,可以把DataSource注册到JdbcTemplate之中. JdbcTemplate 是在JDBC API基础上提供了更抽象的封装,并提供了基于方法注解的事务管理能力. 通过使用SpringBoot自动配置功能并代替我们自动配置beans. 数据源配置 在maven中,我们需要增加spring-boot-starter-jdbc

  • spring boot中使用@Async实现异步调用任务

    什么是"异步调用"? "异步调用"对应的是"同步调用",同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行:异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序.  同步调用 下面通过一个简单示例来直观的理解什么是同步调用: 定义Task类,创建三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10秒内) package com.kfit.task; import java.uti

  • Spring Boot中利用JavaMailSender发送邮件的方法示例(附源码)

    快速入门 在Spring Boot的工程中的pom.xml中引入spring-boot-starter-mail依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> 如其他自动化配置模块一样,在完成了依赖引入之后,只需要在applicatio

  • 详解spring cloud中使用Ribbon实现客户端的软负载均衡

    开篇 本例是在springboot整合H2内存数据库,实现单元测试与数据库无关性和使用RestTemplate消费spring boot的Restful服务两个示例的基础上改造而来 在使用RestTemplate来消费spring boot的Restful服务示例中,我们提到,调用spring boot服务的时候,需要将服务的URL写死或者是写在配置文件中,但这两种方式,无论哪一种,一旦ip地址发生了变化,都需要改动程序,并重新部署服务,使用Ribbon的时候,可以有效的避免这个问题. 前言:

  • 详解Spring Boot中使用Flyway来管理数据库版本

    如果没有读过上面内容的读者,有兴趣的可以一阅.在上面的使用JdbcTemplate一文中,主要通过spring提供的JdbcTemplate实现对用户表的增删改查操作.在实现这个例子的时候,我们事先在MySQL中创建了用户表.创建表的过程我们在实际开发系统的时候会经常使用,但是一直有一个问题存在,由于一个系统的程序版本通过git得到了很好的版本控制,而数据库结构并没有,即使我们通过Git进行了语句的版本化,那么在各个环境的数据库中如何做好版本管理呢?下面我们就通过本文来学习一下在Spring B

随机推荐