背景
项目使用的是boot+dubbo+mybatis的框架。之所以要研究单元测试,并不是因为要自动化测试、提高代码质量、测试覆盖率等高大上的缘由。而是因为环境上的限制,我无法使用热部署(idea社区版,用的人太少,没法子,自己能力不足研究不了),希望通过单元测试的方式来测试自己写的代码。这就要求一个单元测试类的启动最好能在3秒以内。
另外吐槽一下很多写单元测试的博客,丝毫也没有提到实际执行测试需要的时间(不提时间的单元测试都是耍流氓!)。
知识点
mokito
这个单元测试,简单来说就是要mock掉方法中所有依赖别人的地方,相当于仅仅测试代码逻辑(或者说空架子?可能不大贴切)。举个例子,我的项目中大部分逻辑都写在service里面,如下:
public AuthVO getAuthById(int id) {
//所以需要外部支持的部分都不是我本方法需要考虑的内容,比方这个mapper的查询
AdminAuthority adminAuthority = authorityMapper.selectByPrimaryKey(id);
if (Objects.isNull(adminAuthority)) {
throw new VerifyException("不存在此权限");
}
// TODO 接下来要做
return BeanUtils.copy(adminAuthority, AuthVO::new);
}
实际代码逻辑应该比较多,举例子嘛,偷偷懒。
mock的原则是:测试这个方法,就仅仅针对这个方法,所有需要依赖别人的地方都不是此方法需要测试的内容。比方这个mapper的查询selectByPrimaryKey,就需要mock出一个AdminAuthority 对象来替代真实的查询结果。以便这个方法可以不依赖mapper,继续测试。
实际写代码时,对于这个mapper的查询,我们也是需要测试的,如果把这个mock掉,那么我们需要另外的入口去测试(费劲)。同时mock的越多,意味着你需要模拟的越多,花的精力也就越多,当然你程序思考得多,代码质量自然好。不过并不适合开发进度要求快的。特别现在前后端分离,前端等后端,测试等开发。有空的时候写写,还是不错的!
mokito使用
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class AdminServiceImplMockTest {
@Autowired
private AdminService adminService;
@MockBean
private AdminMapper adminMapper;
@BeforeEach
void setUp() {
}
@AfterEach
void tearDown() {
}
@Test
void login() {
Admin admin = new Admin(null,"java1234","123456");
Mockito.when(adminMapper.login(admin)).thenReturn(new Admin(1,"java1234","123456"));
Admin login = adminService.login(admin);
Assertions.assertEquals(login.getId(),1);
Assertions.assertEquals(login.getUsername(),"java1234");
Assertions.assertEquals(login.getPassword(),"123456");
//Mockito的单元测试其实是要启动上下文的
}
}
具体使用参照:
https://mp.weixin.qq.com/s/mOW3Z_Qrq_LuoRWICHeHjQ
简单来说@MockBean 声明了这个bean由mokito去注入而不是spring,debug时可以看到这个adminMapper有“mock”字样。
对于这个测试类,@Autowired private AdminService adminService 是我们需要测试的service,@MockBean private AdminMapper adminMapper 是service里面需要用的mapper。我们不测这个mapper,所以需要mock掉(不依赖数据查询,就不会出现因为数据库原因导致单元测试一段时间后失败的现象)。
那么它是如何是实现的呢?
Mockito.when(adminMapper.login(admin)).thenReturn(new
Admin(1,"java1234","123456"));
这句话的意思是:当调用adminMapper.login(admin)的时候,返回一个我们mock出来的对象。这行代码要在调用adminService.login(admin)之前执行。这样Mockito才能在实际的adminServiceImpl执行到adminMapper.login(admin)的时候,返回我们mock出来的对象(相当于切进去,替换掉)
注:打断点可以看到它进入了adminServiceImpl,并且adminMapper已经被Mockito成功替换,这样返回的对象就是我们mock出来的对象。
mokito注意点
需要注意的是mokito 启动时需要spring的上下文的,并且service只能通过spring进行注入不能自己new
private AdminService adminService = new AdminServiceImpl();
这样写的话,但debug进入AdminServiceImpl里面时,会发现adminMapper是null,而不是Mockito生成的对象。我猜想大概是因为mock出来的adminMapper是由spring容器管理的,adminService 也必须由容器管理,这样才能在进入AdminServiceImpl时adminMapper有值。
需要上下文是指,对于junit4 必须这样写:
因为测的service,不需要web环境所以:
webEnvironment = SpringBootTest.WebEnvironment.NONE
思考点:按理说,mockito 测试就是为了专注方法自己,所有外部依赖一律都mock掉。那么实在没有必要加载上下文,加载上下文就相当于启动了整个项目,什么数据库连接,redis连接,dubbo连接,一堆东西。十几二十秒就得花在这上面了。
优化单元测试启动速度
为单元测试建立自己的启动类
自己的启动类可以设置懒加载,可以注释@EnableDubbo等。总之就是一切单元测试不需要的,真实环境需要的,都可以在这个启动类中去掉。
我建立了两个启动类,一个是mock的(不涉及数据库查询),一个是test(涉及数据库查询)。
注:本意是希望两个test的启动类各子完成自己的事情。但实际测试时发现,一个test启动类上的改动导致启动报错之后,另一个test启动类也同时报错了。。。哎,又是原理。不过,怎么说,至少也能有一个启动类能用
为单元测试建立自己的yml配置文件
注:直接在test下把配置文件copy过来,test启动类就能识别到并执行test下的配置文件。需要注意的是 active: ‘@profileActive@’ 是多配置文件配置,在pom中指定的,在test的yml中不能识别到。所以会报一下类似jdbc url 未定义,但实际有的问题。所以这里直接初始化为active: ‘test’
不同的单元测试类对应自己的配置文件
在测试类加注解@ActiveProfiles(“mock”),即可指定配置文件。奇怪的是,在启动类中加这个注解,虽然不报错,但没有效果。所以只能加在这个地方(BootMockBase作为测试类的父类)
懒加载
boot2.1.0及以下版本通过下面方式实现:
@SpringBootApplication
public class ProviderTestApp {
public static void main(String[] args) {
SpringApplication.run(ProviderTestApp.class, args);
}
//很尴尬的是service是dubbo管理的,mapper是mybatis管理的,下面的配置对它们无效,所以并没有节约多少时间
@Configuration
@ComponentScan(lazyInit = true)
static class LocalConfig{
}
}
boot2.2及以上yml中有配置,网上一堆,就不写了。
注:很尴尬的是service是dubbo管理的,mapper是mybatis管理的,下面的配置对它们无效。所以大部分类都没有能懒加载,所以并没有节约多少时间。
排除dubbo 失败
dubbo会在加载上下文时,输出一堆dubbo连接信息,暴露服务信息,总之一堆消耗了不少时间(大概在8秒内)。
本来的思路是想在单元测试时,将注入的dubbo的service转换成spring的service,省略掉dubbo消耗的时间,毕竟我只测service不需要dubbo。但是能力不足,没办法,但是找到了切入点:
@Configuration
public class BeanPostPrcessorImpl implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(beanName.equals("adminAuthServiceImpl")){
System.out.println("AAAA");
}
System.out.println("对象" + beanName + "开始实例化");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("对象" + beanName + "实例化完成");
return bean;
}
}
思路就是在对象实例化之前的修改为spring的service
我手工替换掉dubbo的service,然后排除dubbo的内容:
在注释掉yml中dubbo的配置后,直接报缺失dubbo application配置的信息:
总结:另外在我排除yml加密的jasypt时发现,即使项目中没有任何地方使用到jasypt。只要pom文件有,端口就会有jasypt的日志信息。这样算不算设计上不合理?我只是引入了这个jar,都没有显式的告诉它我要不要用,它就自动的为我做了一些事情。包括redis 和dubbo,我都没有办法排除掉它们。只能说对boot的原理完全不懂,有时候真的很无力。
我尝试过排除相关的bean,如下:
//正则表达式排除
@ComponentScan(
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com.ulisesbocchio.jasyptspringboot\\..*"
)
}
)
//类排除
@ComponentScan(
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {RedisConfig.class}
)
}
)
结果就是排除貌似失败了,至于@ComponentScan排除失败的问题,网上也有说,不过又是涉及原理的东西。。。我又想到,是否可以在单元测试时,排除引入jar包?网上没查到相关内容,只好作罢。
延迟暴露dubbo服务
无意中看到管理延迟暴露dubbo服务的文章,说的是先完成bean的初始化之后延迟暴露dubbo服务,这样应该可以让单元测试先启动起来。
dubbo:
application:
#注册在注册中心的名称,唯一标识,请勿重复
name: demo-provider
#logger: logback
#单zookeeper服务:zookeeper://127.0.0.1:2181
registry:
address: zookeeper://127.0.0.1:2181
port: 2181 #提供注册的端口
provider:
filter: dubboproviderlogfilter
delay: 500000
注: delay: 500000 时间设置长一些
这样设置之后,启动时间节约了8秒。
其他
可以说本次,想要完成的部分都没有完成,都被原理给卡住了。看来刻不容缓了。