记一次boot+dubbo+mokito 单元测试经历

背景

项目使用的是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秒。

其他

可以说本次,想要完成的部分都没有完成,都被原理给卡住了。看来刻不容缓了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值