单元测试技巧-SpringTest和Mocktio如何优雅Mock Bean

2,153 阅读4分钟

文章内容遵循:

  1. 框架:SpringBoot 2.4.4 , Spring Data JPA
  2. 使用的需求样例参考:业务需求分析详解-如何精确识别并发问题

问题背景

在实际开发中,往往需要依赖很多的第三方服务,但是我们的单元测试,测的是自己写的逻辑,对于需要依赖第三方服务的逻辑,通常会采用Mock。但是结合SpringTest框架的情况下,想要Mock某个Bean有很多种方法,但是哪种方式更好?

实现方案

通过@Mock和@InjectMocks实现


public class UserServiceTest {
    @Mock
    private UserDao userDao;
    @InjectMocks
    private UserService userService;

    @BeforeEach
    public void setup() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testGetUserByName() {
        // mock UserDao 的 getUserByName 方法
        when(userDao.getUserByName("John")).thenReturn(new User("John", 18));
        // 调用 userService 的 getUserByName 方法,期望返回上面 mock 的结果
        User user = userService.getUserByName("John");
        assertNotNull(user);
        assertEquals("John", user.getName());
        assertEquals(18, user.getAge());
    }
}


这种方法能实现Mock,但是每次要使用Mock对象的时候,都需要加上相关的注解,还要在@BeforeEach中加上初始化方法才能使用,操作比较麻烦,而且在团队协作中,多个测试类都需要Mock对象的时候,就会有很多重复代码出现。因此这种实现方式一般情况下不推荐使用。

通过@MockBean注解实现


@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;
    @MockBean
    private UserDao userDao;

    @Test
    public void testGetUserByName() {
        // mock UserDao 的 getUserByName 方法
        when(userDao.getUserByName("John")).thenReturn(new User("John", 18));
        // 调用 userService 的 getUserByName 方法,期望返回上面 mock 的结果
        User user = userService.getUserByName("John");
        assertNotNull(user);
        assertEquals("John", user.getName());
        assertEquals(18, user.getAge());
    }
}


这种方法相比第一种,好处在于减少了获取Mock 对象的操作。但是也有缺点:

  1. 这种实现方式并不能做到统一控制。也就是说,如果期望对这个模拟对象做一些统一处理,或者希望不再Mock 这个对象,因为这个Mock Bean散布在各个测试类中,这时候要更改起来就会比较麻烦
  2. 每个测试类都Mock Bean,对于SpringBootTest来说会创建多个ApplicationContext实例,测试框架中无法使用到ApplicationContext缓存,这样会降低了测试的效率。

通过@Bean注解实现


@Configuration
public class TestConfiguration {
    @Bean
    public UserDao userDao() {
        return Mockito.mock(UserDao.class);
    }
}

@SpringBootTest(classes = TestConfiguration.class)
public class UserServiceTest {
    @Autowired
    private UserService userService;
    @Autowired
    private UserDao userDao;

    @Test
    public void testGetUserByName() {
        // mock UserDao 的 getUserByName 方法
        when(userDao.getUserByName("John")).thenReturn(new User("John", 18));
        // 调用 userService 的 getUserByName 方法,期望返回上面 mock 的结果
        User user = userService.getUserByName("John");
        assertNotNull(user);
        assertEquals("John", user.getName());
        assertEquals(18, user.getAge());
    }
}

这种实现方式相比@MockBean 的方式,好处在于可以统一控制Mock对象的行为,而且只需要定义一次Mock对象即可,使用时和其他的Bean使用无异。 而缺点是,不同的 Mock 对象需要在不同的配置类中定义,管理起来可能比较麻烦。不过,我们可以对需要Mock的对象进行分类,对于需要依赖的第三方服务,必定是需要Mock的,可以将它们的Mock配置类统一导入,而对于可一些可选的Mock对象,则可以选择性地使用。

比如,假设除了UserService 之外,还有用于发送消息给前端/客户端的功能,对应的Service也需要Mock,那可以对这两种服务的Mock对象分开定义:

@TestConfiguration
public class UserServiceMockConfiguration {
    @Bean
    public UserService mockUserService() {
        return Mockito.mock(UserService.class);
    }
}


@TestConfiguration
public class RealTimeMessageServiceMockConfiguration {
    @Bean
    public RealTimeMessageService mockRealTimeMessageService() {
        return Mockito.mock(RealTimeMessageService.class);
    }
}

假设获取用户信息是需要统一Mock的,而发送消息是可以有选择性地Mock的,那么在使用的时候,可以这么定义:

// 对于Mock的 UserService的使用
@Import({  
	 UserServiceMockConfiguration.class
})  
public class MockServiceTestConfiguration {  
  
}


然后在spring.factories文件中增加这个配置,使其在Spring启动时就能加载到这个配置:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
com.peng.project.test.MockServiceTestConfiguration

而对于RealTimeMessageService来说,则可以在使用时才加上相关的配置:

@SpringBootTest(classes = RealTimeMessageServiceMockConfiguration.class)
public class UserServiceTest {

	@Test
	public void test() {
		// ...
	}
}

如果测试类已经继承了一个公共的BaseTest,不好重写@SpringBootTest的话,也可以这样直接引用:


@Import({
	RealTimeMessageServiceMockConfiguration.class
})
@Component
public class Prepare {

}

public class CallApplicationServiceTest extends BaseTest {

	@Autowired
	private Prepare prepare;

	@Test
	public void test() {
		// ...
	}
}


这种实现方式相对来说就比较灵活,推荐使用。

通过BeanPostProcessor扩展点实现


@Component
public class MockBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof UserDao) {
            return Mockito.mock(UserDao.class);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {UserService.class})
public class UserServiceTest {

    @Autowired
    private UserService userService;
    
    @Autowired
    private UserDao userDao;

    @Test
    public void testGetUserByName() {
        // mock UserDao 的 getUserByName 方法
        when(userDao.getUserByName("John")).thenReturn(new User("John", 18));
        // 调用 userService 的 getUserByName 方法,期望返回上面 mock 的结果
        User user = userService.getUserByName("John");
        assertNotNull(user);
        assertEquals("John", user.getName());
        assertEquals(18, user.getAge());
    }
}


这种方式也能达到对Bean对象的统一管控的效果,但是相比@Bean注解实现的方案来说,只要用了这种实现方式,在所有的测试类中就一定会用到Mock对象,这也是和@Bean注解实现有差距的地方。

总结

总的来说 ,@Bean注解实现Mock对象的方案相对来说是比较灵活的,而且使用上也不需要开发者需要过多地关注如何Mock一个对象,相比其他三种方案来说是较优雅的,也已经满足大部分的场景了,推荐以@Bean注解的方式作为项目中的默认Mock方式。如果有@Bean注解不满足的场景,那可以再考虑用其他实现方式作为补充。