spring中使⽤Mockito解决Bean依赖树问题⽅法
前提
本⽂不是针对Mockito的⼊门教学,主要叙述如何简单的使⽤Mockito解决Bean依赖树问题,对于Mockito的学习请其他的⽂章或者查阅官⽅⽂档
基本概念 Junit初始化及存在的问题
spring应⽤在unit test时,test是独⽴运⾏的,所以需要⾃⾏ init ApplicationContext,启动 Ioc容器。
Junit要求:Test类中涉及的所有Spring bean 注⼊成功才能完成applicationContext初始化,并启动IOC容器,否则⽆法执⾏unit test。
ApplicationContext初始化的两种⽅式⼿动注⼊(使⽤ @Bean或者 @Component 注⼊所需的类)编写@Configuration 类(使⽤@ComponentScan 指定扫描beans)两种初始化⽅式存在的问题
⽅式⼀:
所需的beans中,⼀个bean少注⼊了就会导致⽆法初始化上下⽂需要注⼊的bean太多时,需要花费⼤量的时间和精⼒,排查缺漏难度⼤
⽅式⼆:
颗粒度难以把控,随着项⽬规模变⼤之后,可能导致bean导⼊过多,单元测试跑很久才能通过当项⽬规模⼤了之后,bean之间的依赖往往是复杂的,扫描bean的⽅式可能出现⼀些不属于⾃⼰模块的未知问题或者某些中间件在unitTest环境⽆法正常启动,导致⽆法初始化上下⽂什么是依赖树?
在开发应⽤时,往往会出现如上图的树型依赖,⽐如 serviceA 调⽤ serviceB,serviceB ⼜调⽤ serviceC 。spring ioc注解
然⽽这只是⼀个简单的例⼦。真正的开发中,往往⼀个 service 会依赖多个 service ,以及多个 dao ,以此来实现业务逻辑。
⽽根据Junit要求,我们必须将树的路径经过的所有节点(bean)都注⼊才能完成spring上下⽂初始化。这时如果bean之间的依赖耦合过⼤时,就⽆法跳脱出两种初始化⽅式带来的问题。
什么是Mockito?
在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取⽐较复杂的对象(如 JDBC 中的ResultSet 对象),⽤⼀个虚拟对象(Mock 对象)来创建以便测试的测试⽅法。
Mock 最⼤的功能是帮你把单元测试的耦合分解开,如果你的代码对另⼀个类或者接⼝有依赖,它能够帮你模拟这些依赖,并帮你验证所调⽤的依赖的⾏为。
简单来说:就是虚拟⼀个mock对象,这个对象在单元测试时会“狸猫换太⼦”,将原有bean进⾏替换,“骗过”spring初始化,成功启动ioc容器,以此规避常规初始化⽅式带来的种种问题。
开发场景
结合本⼈在⼯作中遇见的问题,当时我所写的模块进⾏unitTest时,就出现了依赖树过于庞⼤的问题。
⾸先,我采⽤了常规的⼿动注⼊(⽅式⼀),导致注⼊了很久都没注⼊完,⽆法执⾏测试。后来觉得这⽅法在这种情况不可⾏。然后,我采⽤了编写@Configuration 类(⽅式⼆),同样也存在⼀些问题。⼀些不属于我负责模块的bean也被注⼊,其中某些涉及TaskSchedule的bean⽆法被正确注⼊,导致⽆法执⾏测试。此时⼀个个bean探索,解决问题显然不现实。最后,我采⽤Junit+Mockito结合的
⽅式进⾏单元测试。按照依赖树⼤⼩进⾏区分。依赖树⼩的直接使⽤常规的⼿动注⼊(⽅式⼀),省事,同时保证⼤部分逻辑按照代码正常运⾏依赖树⼤的使⽤Mockito,避免前⽂提到的两种初始化⽅式导致的问题
使⽤ 1 导⼊maven依赖
⾸先导⼊mockito maven依赖,版本请根据⾃⼰的spring版本选择,否则会出现不兼容的情况。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
注意:
此处导⼊了spring-boot-starter-test是因为这个依赖已经包含了mockito相关的jar包
spring-boot-starter-test可以使⽤ @MockBean 注解(mockito-core、mockito-all貌似不能)
@Mock和@MockBean的区别:
@Mock @MockBean mock bean替换时机 spring上下⽂初始化完成之后 spring上下⽂初始化执⾏期间能否“骗”过spring初始化否是能否解决依赖树否是在没注⼊所有所需的bean,⽆法完成spring上下⽂初始化时,@Mock⽆法正常⼯作 @MockBean在初始化时就进⾏替换,spring上下⽂初始化时检测的bean为替换后的mock bean,⽽mock bean本⾝是⽆依赖任何其他bean的,⾃然能够“骗”过spring 上下⽂初始化阶段,成功启动IOC容器 2 分析bean之间的依赖
使⽤⼀个简单的Demo进⾏开发场景的模拟,采⽤Junit+Mockito结合的⽅式进⾏单元测试,根据依赖树⼤⼩区分出是否需要mock
如图,此处编写了⼀个ControllerA,ControllerA中依赖了2个bean:ServiceA,DaoA
分析过程:关于 DaoA :由于Dao往往不会依赖其他的bean,所以此处可以使⽤常规的⼿动注⼊(⽅式⼀)即可。⽅便快捷关于 ServiceA :由于serviceA依赖了serviceB(->DaoB)、serviceC(->DaoC),像这样的嵌套依赖的bean就可以使⽤Mockito,来解决依赖树问题 3 编写Test类
daoA使⽤@Bean注解注⼊即可
@Bean
public DaoA daoA(){
return new DaoAImpl();
}
1.serviceA⾸先使⽤@MockBean注解,将serviceA模拟为Mock Bean,它将在spring上下⽂初始化时就替换掉原有Bean
@MockBean
private ServiceA serviceA;
2.在test类执⾏前(@Before),使⽤Mockito API设置调⽤某个⽅法的返回值(你预期得到的返回结果),在Test类中调⽤这个⽅法时就会返回所指定的值
@Before
public void init(){
MockitoAnnotations.initMocks(this);//只使⽤ @MockBean 时可省略这句
when(controllerA.serviceA_method()).thenReturn("666");
}
3.使⽤ @InjectMocks 通知依赖了serviceA的controllerA,在spring启动时,对controllerA这个bean进⾏相应的后置处理
@Autowired
@InjectMocks
private ControllerA controller;
4.单元测试时,就不会使⽤原有Bean的⽅法,⽽是使⽤Mock Bean及其已经指定了返回值的⽅法@Test
public void testDeepMock() {
String s = controllerA.serviceA_method();
System.out.println(s);
}
5.unitTest结果
以上就是本次介绍的全部相关知识点,感谢⼤家的学习和对的⽀持。