阿毛
It's me !
想你所想
单元测试之私有方法mock

mockito大家都比较熟悉了,存在或者不存在,都不要紧,mockito让你有一种只要一出手,就知道有没有的感觉。但是它也不是万能的,比如静态方法、私有方法,它就无能为力了。这是为什么呢?当然不是mockito的框架或现有技术解决不了,而是出于某些原因或立场,比如测试理念观点。甚至在mockito的FAQ中,作者明确了每一项未实现的功能不支持的原因,或者干脆说已经有别的工具实现了,需要的话,去用那个工具吧,我不愿意重复造轮子。

当然实现这些也并非轻而意举,比如如何mock final类,特别是jdk中的final类,比如String。但作为系统类,在任何时候都不应该可以被修改(即使是有办法修改,也不建议去修改,也没有必要修改,否则重新设计一门新语言即可),特别是对于java.lang包下的类,如基本的数据类型Integer、Long等。java agent可以修改由AppClassLoader加载的类,而endorsed技术也只允许覆盖在有限的限制列表中的类。而powermock采取的方案是,如果需要mock的是系统类的final方法和静态方法,PowerMock不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。

mockito的实现原理是用asm给需要mock的对象生成对应的代理对象,然后使用mock出来的对象即可。而在spring框架中,SpyBean与MockBean的原理也是一样,只不过还需要多做一步,就是用mock后的对象替换容器中原有的对象。

一、为什么要使用PowerMock

      现如今比较流行的Mock工具如jMock EasyMock 、Mockito等都有一个共同的缺点:不能mock静态、final、私有方法等。而PowerMock能够完美的弥补以上三个Mock工具的不足。

二、PowerMock简介

      PowerMock是一个扩展了其它如EasyMock等mock框架的、功能更加强大的框架。PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。通过使用自定义的类加载器,简化采用的IDE或持续集成服务器不需要做任何改变。熟悉PowerMock支持的mock框架的开发人员会发现PowerMock很容易使用,因为对于静态方法和构造器来说,整个的期望API是一样的。PowerMock旨在用少量的方法和注解扩展现有的API来实现额外的功能。目前PowerMock支持EasyMock和Mockito。

三、PowerMock入门  

      PowerMock有两个重要的注解:

      –@RunWith(PowerMockRunner.class)

      –@PrepareForTest( { YourClassWithEgStaticMethod.class })

如果有调用其他类中的静态方法,则需要指定被mock的静态资源类,没有的话则不需要

如果你的测试用例里没有使用注解@PrepareForTest,那么可以不用加注解@RunWith(PowerMockRunner.class),反之亦然。当你需要使用PowerMock强大功能(Mock静态、final、私有方法等)的时候,就需要加注解@PrepareForTest。

先给出代码示例:

package cn.susoncloud.wisdomclass.usercenter;

import cn.susoncloud.wisdomclass.usercenter.domain.service.TenantService;
import cn.susoncloud.wisdomclass.usercenter.domain.service.UserService;
import cn.susoncloud.wisdomclass.usercenter.interfaces.assembler.LoginAssembler;
import cn.susoncloud.wisdomclass.usercenter.interfaces.assembler.RedisUserAssembler;
import cn.susoncloud.wisdomclass.usercenter.interfaces.dto.UserLoginPwd;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.powermock.api.mockito.PowerMockito;
import static org.mockito.ArgumentMatchers.any;

/**
 * @Description: 工具测试方法
 * @Author: huminghao
 * @Date: 2020-02-03
 */
@ActiveProfiles("testDevelopSql")
@SpringBootTest
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})
@PrepareForTest(LoginAssembler.class)
public class DemoTest {

    @Autowired
    RedisUserAssembler redisUserAssembler;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private UserService userService;

    @Autowired
    private TenantService tenantService;

    /**
     * @Description: 根据手机号获取token,mock掉了其余步骤,如密码解密验证。
     * @Return: void
     * @Author: huminghao
     * @Date: 2020/2/3
     */
    @Test
    public void getTokenForTestByPwd() throws Exception {
        LoginAssembler loginAssembler = new LoginAssembler();
        loginAssembler.setRedisUserAssembler(redisUserAssembler);
        loginAssembler.setRedisTemplate(redisTemplate);
        loginAssembler.setUserService(userService);
        loginAssembler.setTenantService(tenantService);
        LoginAssembler spyLoginAssembler = PowerMockito.spy(loginAssembler);
        PowerMockito.doNothing().when(spyLoginAssembler, "checkLoginPwd", any(), any());
        UserLoginPwd userLoginPwd = new UserLoginPwd();
        userLoginPwd.setPhoneNumber(4L);
        userLoginPwd.setPassword("");
        userLoginPwd.setClientId("");
        userLoginPwd.setLoginChannel(2);
        System.out.println(spyLoginAssembler.pwdLogin(userLoginPwd));
    }

}

在 Spring Boot 的测试套件中,需要添加 @RunWith(SpringRunner.class) 和 @SpringBootTest 注解。

但是 PowerMock 需要添加 @RunWith(PowerMockRunner.class) 注解。

@RunWith 注解只能有一个,解决方案是使用 @PowerMockRunnerDelegate 注解,同时使用 @PowerMockIgnore 注解避免报错。

上述示例中,因为我们需要mock掉LoginAssembler的私有方法checkLoginPwd,无论传入什么参数,都不进行真实调用,避免真实的密码校验逻辑,而其他方法正常真实调用。所以先@PrepareForTest指定私有方法的类,然后spy(注意这里的spy和spring的@SpyBean行为并不一致,他不会容器注入,需要我们手动bean管理,如上代码该类的属性均为手动赋值,这样才能保证我们其他方法真实调用时,不会出错。因为方法无返回值,所以doNothing,注意doNothing需要指定在when之前,否则when中指定的方法,会先真实调用一次。any为mockito的api)

 四、PowerMock简单实现原理

       •  当某个测试方法被注解@PrepareForTest标注以后,在运行测试用例时,会创建一个新的org.powermock.core.classloader.MockClassLoader实例,然后加载该测试用例使用到的类(系统类除外)。

       •   PowerMock会根据你的mock要求,去修改写在注解@PrepareForTest里的class文件(当前测试类会自动加入注解中),以满足特殊的mock需求。例如:去除final方法的final标识,在静态方法的最前面加入自己的虚拟实现等。

       •   如果需要mock的是系统类的final方法和静态方法,PowerMock不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。

powermock使用了自定义的classloader来解决mock静态方法与私有方法的问题,因此其会为加了PrepareForTest注解的类生成对应的classloader来加载用到的类,这样就可能会导致其与系统的classloader加载了相同的类,导致类型转换失败,PowerMockIgnore注解则是告诉powermock放弃加载指定的这些类。

参考:PowerMock 精萃

发表评论

textsms
account_circle
email

想你所想

单元测试之私有方法mock
mockito大家都比较熟悉了,存在或者不存在,都不要紧,mockito让你有一种只要一出手,就知道有没有的感觉。但是它也不是万能的,比如静态方法、私有方法,它就无能为力了。这是为什么呢?…
扫描二维码继续阅读
2020-06-14