Springboot 如何使用Powermock做单元测试

11 篇文章 2 订阅
4 篇文章 0 订阅

Powermock介绍

一、为什么要使用Mock工具

在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,比如:(发送邮件,网络通讯,远程服务, 文件系统等等)。 而我们没法控制这些外部依赖的对象,为了解决这个问题,我们就需要用到Mock工具来模拟这些外部依赖的对象,来完成单元测试。

二、PowerMock简介

PowerMock 也是一个单元测试模拟框架,它是在其它单元测试模拟框架的基础上做出的扩展。通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock 现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持,对静态初始化过程的移除等强大的功能。因为 PowerMock 在扩展功能时完全采用和被扩展的框架相同的 API, 熟悉 PowerMock 所支持的模拟框架的开发者会发现 PowerMock 非常容易上手。PowerMock 的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能。
简而言之,Powermock是mockito的升级版,但powermock会依赖于mockito。

使用示例

Maven 包引入

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <powermock.version>2.0.0</powermock.version>
</properties>

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>${powermock.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>${powermock.version}</version>
    <scope>test</scope>
</dependency>

简单使用

HelloMockitoTest

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)

//@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@PowerMockIgnore("javax.management.*")
@PrepareForTest({HelloController.class, TestUtils.class})
@ContextConfiguration(classes = TestBizServer.class)
@ActiveProfiles("local")
public class HelloMockitoTest {

    @InjectMocks
    private HelloController helloController;

    @Mock
    private HelloService helloService;

    @Before
    public void init () {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void test () throws Exception {
        Mockito.when(helloService.sayHi(Mockito.any(String.class))).thenReturn("你好啊!");
        String result2 = helloController.testHello();
        System.out.println(result2);
        assert result2.equals("你好啊!");
    }

    @Mock
    private TestUtils testUtils;

    @Test
    public void testSome () throws Exception {
        TestUtils mock = Mockito.mock(TestUtils.class);
        PowerMockito.whenNew(TestUtils.class).withNoArguments().thenReturn(mock);
        Mockito.when(mock.testSome()).thenReturn("123456");
        String s = helloController.doHttpGet("http://127.0.0.1", Maps.newHashMap());
        System.out.println(s);
    }
}

HelloController 这里代码都是伪代码,不要把controller当成一个rest,当成一个Bean即可。

@RestController
@RequestMapping("/hello")
public class HelloController {

    @Autowired(required = false)
    HelloService helloService;

    @GetMapping("/test")
    public String testHello () {
        String hello = null;
        try {
            Thread.sleep(1000);
            hello = helloService.sayHi("hello world");
            System.out.println("hello param is : " + hello);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return hello != null ? hello : "error";
    }

    @GetMapping("/testSome")
    public String doHttpGet(String uri, Map<String, String> getParams) {
        TestUtils testUtils = new TestUtils();
        String s = testUtils.testSome();
        System.out.println("testUtils.testSome return is:" + s);
        CloseableHttpResponse response = null;
        return s;
    }
}

HelloService

@Service
public class HelloService implements InitializingBean {

    @Autowired
    private HelloDao helloDao;

    public String sayHi () {
        System.out.println("HI ...");
        return "hi";
    }

    public String sayHi (String name) {
        System.out.println("HI ..." + name);
        return "hi";
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("我是动态注册的你,不是容器启动的时候注册的你");
    }

    public boolean deleteOne () {
        System.out.println("------------------------------");
        return helloDao.deleteOne();
    }
}

test方法运行如果没有写mock正常会返回HI,这里会返回你好啊!。mockito这中mock方式只适用于Spring中注入代理的Bean才能进行正常的mock。如果说这方法体中实例化了对象或者调用静态方法,这个时候就不起作用了。

testSome中使用了powermock去处理在方法中实例化的新对象,把new对象替换为mock对象(可以理解为一层代理 ),然后让调用mock对象的方法返回我们想要的值。

其实这里可以理解,如果我们想让某个对象的方法返回一个值或者抛出一个异常,我们就需要把这个对象替换成mock对象,可以把mock对象理解为代理对象(打桩),这个是核心思路。

注解讲解

@RunWith

在测试类类名上添加 @RunWith(PowerMockRunner.class) 注解代表该测试类使用 PowerMock。必须添加

@PrepareForTest

这个注解的作用就是告诉 PowerMock 哪些类是需要在字节码级别上进行操作的。也就是需要 mock 某些包含 final、static 等方法的类时使用,使用方法:@PrepareForTest({System.class, LogUtils.class}),在方法中有new对象的时候也需要在@PrepareForTest中声明。

@PowerMockIgnore

PowerMock 是使用自定义的类加载器来加载被修改过的类,从而达到打桩的目的。@PowerMockIgnore 注解告诉 PowerMock 忽略哪些包下的类,从而消除类加载器引入的 ClassCastException。使用方法:@PowerMockIgnore({“javax.management.”, “javax.net.ssl.”, “javax.script.*”})

@SuppressStaticInitializationFor

告诉 PowerMock 哪些包下的类需要被抑制静态初始化,包括 static 代码块或者 static 变量的初始化。防止因静态初始化导致的错误。使用方法:

@SuppressStaticInitializationFor({com.xxx.SmsServiceImpl})

@Mock
mock 待测类的普通属性,最常见的就是通过 Spring @Autowired 自动注入的 bean。这类属性并非 final,也并非 static,只需要在测试类中使用 @Mock 注解,Pwermock 框架就能自动生成 mock 对象,并自动注入到 @InjectMock 修饰的待测类中。

也可以使用比较通用的 mock() 方法,调用 mock(XXX.class),能生成一个 mock 的 XXX 对象,然后通过反射获取待测类的字段,再将 mock 对象赋给字段。一般用于不是由 Spring @Autowired 注入的普通属性。

PowerMock简单实现原理

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

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

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

Api讲解

whenNew 当创建新对象的时候

PowerMockito.whenNew(Entity.class).withAnyArguments().thenReturn(entity);

thenThrow 抛出异常
when 当执行xx方法的时候,一般后面跟链式调用处理分支
doNothing 什么都不执行的时候,一般后面跟链式调用处理分支
doReturn 返回,一般后面接when(object).xxxMethod(), 意指让mock对象调用某些方法的时候不做任何处理。

PowerMockito.doNothing().when(userMappers).putUser(Mockito.any(UserDTO.class));  //主要代码

thenReturn 然后返回
mock 创建mock对象
mockStatic mock静态class方法属性,要在类前@PrepareForTest中加入使用SendEmailProxy的类。

PowerMockito.mockStatic(SendEmailProxy.class);
PowerMockito.when(SendEmailProxy.doSomething()).thenReturn(xxx);

any 模拟参数

PowerMockito.any(xxx.class)

注意

  • 如果项目中增加了springboot-actuator包则需要在类前加下述注解配置
@PowerMockIgnore({"javax.management.*"})
  • 如果需要测试HttpComponent包则需要在类前加下述注解配置
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})

Powermock与Jacoco兼容问题

如果在@PrepareForTest中修饰了一个类,这个类jacoco就没法进行覆盖率统计了,powermock是与jacoco的on-the-fly模式冲突的,但是有办法可以避免这样的问题。

原因

JaCoCo和PowerMock都是通过在加载类的时候修改字节码文件来实现统计覆盖率和mock静态类的功能。JaCoCo在加载class的时候会把统计代码插入到class中,而PowerMock当使用了@PrepareForTest注解,在加载相关类的时候会从class文件重新读取字节码信息,导致JaCoCo的修改都没有了,所以就没办法统计到了

on-the-fly模式的解决方案

1.不是用@PrepareForTest修饰要测覆盖率的类,往需要覆盖的类底层去mock,比如 A 依赖 B ,B依赖C,C依赖D。加入我们需要得到A和B的覆盖率,那么我们在@PrepareForTest中就只添加C和D,只去mock C和D即可。
2.上述例子,假如我们A没有依赖,但A中有成员变量需要mock,这个时候我们可以利用反射将成员变量设置为mock对象。然后再进行mock操作,这样就避免了再@PrepareForTest中加入A.class导致覆盖率为零。

缺点:
1.这样写对开发人员的要求比较高,要熟悉各种类库(JDK、各种中间件),往代码深处去mock代理取代在需要单元测试覆盖率的类中进行代理测试。
2.有些底层类不支持mock,比如Charset 的静态方法中要求encode参数必须不能为空,powermock就没法代理到Charset.encode的调用。

offline解决方案

使用jacoco的offline模式:

<!--- 定义 jacoco 版本 -->
<properties>
  <jacoco.version>0.8.5</jacoco.version>
</properties>
<!--- 定义 jacoco 执行 offline 模式 goals -->
<build>
  <plugins>
    <!-- 注意不是在pluginManagement, pluginManagement中只是声明 -->
    <plugin>
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <version>${jacoco.version}</version>
      <executions>
        <execution>
          <id>default-instrument</id>
          <goals>
            <goal>instrument</goal>
          </goals>
        </execution>
        <execution>
          <id>default-restore-instrumented-classes</id>
          <goals>
            <goal>restore-instrumented-classes</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <!--离线模式必需指定, 否则到模块根目录而不是target目录了-->
        <configuration>
            <systemPropertyVariables>
                <jacoco-agent.destfile>target/jacoco.exec</jacoco-agent.destfile>
            </systemPropertyVariables>
        </configuration>
    </plugin>
  </plugins>
</build>
<!--- 定义 jacoco 依赖  注意不是在dependencyManagement, dependencyManagement中只是声明-->
<dependencies>
  <dependency>
    <groupId>org.jacoco</groupId>
    <artifactId>org.jacoco.agent</artifactId>
    <version>${jacoco.version}</version>
    <classifier>runtime</classifier>
  </dependency>
</dependencies>

Offline模式单元测试不能跨模块, 不能源码在A模块单测写在B模块
比方说:项目分成了四个模块,service,utils,dao,controller,你在写service单测的时候哪怕调用了utils的代码,但是在实际统计覆盖率的时候是没办法统计到的,需要单独针对每一个模块写单测

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

澄风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值