文章内容遵循:
- 框架:SpringBoot 2.4.4 , Spring Data JPA
- 使用的需求样例参考:业务需求分析详解-如何精确识别并发问题
问题背景
在实际开发中,往往需要依赖很多的第三方服务,但是我们的单元测试,测的是自己写的逻辑,对于需要依赖第三方服务的逻辑,通常会采用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 对象的操作。但是也有缺点:
- 这种实现方式并不能做到统一控制。也就是说,如果期望对这个模拟对象做一些统一处理,或者希望不再Mock 这个对象,因为这个Mock Bean散布在各个测试类中,这时候要更改起来就会比较麻烦
- 每个测试类都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注解不满足的场景,那可以再考虑用其他实现方式作为补充。