Spring 单元测试-PowerMockRunner和SpringRunner

本文介绍了Spring单元测试中的PowerMockRunner和SpringRunner,阐述了它们在测试中的作用和应用场景。SpringRunner适用于常规的Spring集成测试,而PowerMockRunner则能mock静态方法、私有方法等,提高了测试效率。文章通过示例展示了如何使用这两个工具,并讨论了它们的优缺点以及在项目中的实际应用问题,如与Jacoco覆盖率插件的兼容性问题。

概述

  我们在开发过程中,为了代码的稳定性也好,为了少给自己以后的开发挖坑也好,多写单元测试绝对是一件性价比超高的繁琐事,既然是繁琐的事情,我想大部分人是不愿意写的,我也不愿意写,但是要做好一个程序员,不仅仅的去做一个低级码农,那就从最简单的地方做起,单元测试就是一件特别简单的事。写了单元测试的代码的健壮性和逻辑性绝对要更上一个层次,而且对于开发而言理解回顾代码逻辑是一件必不可少的事情
  一个 bug 被隐藏的时间越长,修复这个 bug 的代价就越大。前期多去写一些边界测试,后期就有时间学习,开发时间和回归回顾时间的比例应该是1:1,单元测试是一个方法层面上的测试,也是最细粒度的测试。用于测试一个类的每一个方法都已经满足了方法的功能要求,可以避免测试点的遗漏,为了更好的进行测试,可以提高测试效率。在开发中,对于自己开发的模块,只有在通过单元测试之后,才能提交到 SVN 库 或者 Git 库。

工具

  我将在下面介绍下PowerMockRunner和SpringRunner两个单元测试的运行环境。

SpringRunner

  SpringRunner 继承了SpringJUnit4ClassRunner,SpringRunner是SpringJUnit4ClassRunner的一个别名,没有扩展任何功能。下面我们来看下示例

package com.hly.unitest.controller;

import com.alibaba.fastjson.JSON;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceNoMockTest {

    @Autowired
    private UserService userService;

    @Test
    public void getUser() throws Exception {
        UserInfo result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
        System.out.println("the 1st time: result = " + JSON.toJSONString(result));
    }
}

返回显示:
在这里插入图片描述
分析:
首先我们分析下注解:
@RunWith: 用于指定junit运行环境,是junit提供给其他框架测试环境接口扩展,为了便于使用spring的依赖注入,spring提供了org.springframework.test.context.junit4.SpringJUnit4ClassRunner作为JUnit测试环境。在JUnit中有很多个Runner,他们负责调用你的测试代码,每一个Runner都有各自的特殊功能,你要根据需要选择不同的Runner来运行你的测试代码。我们此篇文章只探讨SpringRunner和PowerMockRunner。
@SpringBootTest: 替代了spring-test中的@ContextConfiguration注解,目的是加载ApplicationContext,启动spring容器@SpringBootTest注解会自动检索程序的配置文件,检索顺序是从当前包开始,逐级向上查找被@SpringBootApplication或@SpringBootConfiguration注解的类。

提出问题: 这样的测试方式会连接数据库,进行网络请求等等耗时操作,甚至数据库中的准备数据是个非常麻烦的事情,各个逻辑分支如果都需要测试,这在大型的项目中工作量是不可想象的,所以有没有可能对数据库这种操作直接不处理,这需要mock能力,为此引入了MockBean,下面来看示例:

package com.hly.unitest.controller;

import com.alibaba.fastjson.JSON;
import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

import static org.mockito.ArgumentMatchers.any;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;
    @MockBean
    private UserInfoMapper userInfoMapper;

    @Test
    public void getUser() {
        UserInfo result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
        System.out.println("the 1st time: result = " + JSON.toJSONString(result));

        UserInfo userInfo = mockUserInfo();
        Mockito.when(userInfoMapper.selectByUserId(any())).thenReturn(userInfo);

        result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
        System.out.println("the 2nd time: result = " + JSON.toJSONString(result));
    }

    private UserInfo mockUserInfo() {
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId("654321");
        userInfo.setUserName("B");
        userInfo.setPassword("pwB");
        userInfo.setGender("女");
        return userInfo;
    }
}

返回显示:
在这里插入图片描述
分析:
  我们在UserInfoMapper上加了注解@MockBean,这个注解的意思所有的UserInfoMapper的方法都被mock,返回都将变成null,所以第一次返回值为null;我们在测试方法中借助Mockito.when将userInfoMapper.selectByUserId的返回值进行自定义,这样就完成了对数据库的返回对象的控制,不用怕数据库的更改导致测试用例的分支没有跑完成的问题出现。

提出问题: 我们发现这样的处理虽然符合了上个问题的预期,但是又有了新的问题出现,第一:既然将数据库和网络操作全部mock,那好像就没有必要启动Spring,@MockBean有个特别大的危害就是导致频繁的启动Spring,而spring boot 的启动时间比较耗时,所以@MockBean,很有可能导致测试运行的很慢;第二:如果有这样一个方法,是UserService 的私有方法,里面特别复杂的操作,我并不想在这个测试用例中进行测试,可以不可以mock,或者有一个静态类,类中的方法有没有方式mock,还有UserService的变量我又没有方式可以直接赋值等等,这些SpringRunner也就是SpringJUnit4ClassRunner都是没有办法解决的,这需要下面的有个JUNIT工具解决,也就是PowerMockRunner。

PowerMockRunner

  PowerMock基本上cover了所有Mockito不能支持的case(大多数情况也就是静态方法,但其实也可以支持私有方法和构造函数的调用)。PowerMock使用了字节码操作,因此它是自带Junit runner的。在使用PowerMock时,必须使用@PrepareForTest注释被测类,mock才会被执行。下面我们来看示例:
对UserService进行单元测试:

package com.hly.unitest.service;

import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.model.UserInfoRequest;
import com.hly.unitest.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;

@Service
public class UserService {

    private String finalStr = "finalStr";
    @Autowired
    private UserInfoMapper userInfoMapper;

    public void save(UserInfoRequest request) {

        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(request.getUserId());
        userInfo.setUserName(request.getUserName());
        userInfo.setPassword(request.getPassword());
        userInfo.setGender(request.getGender());
        Date currDate = CommonUtil.getMaxDayOfThisMonth(new Date());
        userInfo.setCreateTime(currDate);
        userInfo.setUpdateTime(currDate);
        userInfoMapper.insertSelective(userInfo);
    }

    public UserInfo getUserInfo(String userId) {
        if (CommonUtil.isEmpty(userId)) {
            return null;
        }

        String assetStr = getAssetStr0("a", "b");
        if (!"un".equals(assetStr)){
            System.out.println("assetStr = " + assetStr);
            return null;
        }
        getAssetNull();

        UserInfo userInfo = userInfoMapper.selectByUserId(userId);
        return userInfo;
    }

    public UserInfo getUser(String userId) {
        UserInfo userInfo = userInfoMapper.selectByUserId(userId);
        return userInfo;
    }

    public String testStaticMethod() {

        String randomStr = CommonUtil.generateUUID();
        return randomStr;
    }

    public String testClassVariables() {

        return finalStr;
    }

    private String getAssetStr() {
        System.out.println("getAssetStr");
        return "unit-test";
    }

    private String getAssetStr0(String a, String b) {
        System.out.println("getAssetStr0");
        return "unit-test";
    }

    private void getAssetNull() {
        System.out.println("getAssetNull");
        return;
    }
}

单元测试如下:

package com.hly.unitest.controller;

import com.alibaba.fastjson.JSON;
import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import com.hly.unitest.util.CommonUtil;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import org.springframework.test.util.ReflectionTestUtils;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class, CommonUtil.class})
public class UserServicePowerRunnerTest {

    @InjectMocks
    private UserService userService;
    @Mock
    private UserInfoMapper userInfoMapper;

    @Before
    public void setUp() {
        // mock私有方法
        userService = PowerMockito.spy(new UserService());
        ReflectionTestUtils.setField(userService, "userInfoMapper", userInfoMapper);
    }

    @Test
    public void getUserInfo() throws Exception {
        UserInfo userInfo = mockUserInfo();
        when(userInfoMapper.selectByUserId(any())).thenReturn(userInfo);

        // 控制私有方法的返回值
        PowerMockito.doReturn("un").when(userService, "getAssetStr0", any(), any());

        UserInfo result = userService.getUserInfo("6f49aa9e-1afc-4439-ad21-25ae90dde566");
        System.out.println(JSON.toJSONString(result));
    }

    @Test
    public void testStaticMethod() throws Exception {
        String result = userService.testStaticMethod();
        System.out.println("the 1st time: result = " + result);

        // mock静态方法
        PowerMockito.mockStatic(CommonUtil.class);
        String str = "123";
        when(CommonUtil.generateUUID()).thenReturn(str);
        result = userService.testStaticMethod();
        System.out.println("the 2nd time: result = " + result);
    }

    @Test
    public void testClassVariables() throws Exception {
        String result = userService.testClassVariables();
        System.out.println("the 1st time: result = " + result);

        // mock类变量
        Whitebox.setInternalState(userService, "finalStr", "FINAL-STR");
        result = userService.testClassVariables();
        System.out.println("the 2nd time: result = " + result);
    }

    private UserInfo mockUserInfo() {
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId("654321");
        userInfo.setUserName("B");
        userInfo.setPassword("pwB");
        userInfo.setGender("女");
        return userInfo;
    }
}

测试getUserInfo返回:
在这里插入图片描述
测试testStaticMethod返回:
在这里插入图片描述
测试testClassVariables返回:
在这里插入图片描述
分析:
  可以看到各个测试用例的返回都符合预期,而且没有启动Spring,这极大的缩短了执行时间,方便了测试用例的编写,所以目前来看,我推荐大家使用PowerMockRunner,轻量级的使用,测试覆盖分支也是极方便的。

提出问题: 我们项目组的覆盖率插件使用的是jacoco,但是PowerMockRunner和jacoco不兼容,使得我们项目的覆盖率一直在10%左右徘徊,对于怎么解决这个问题,一直以来,头特别的大,在查了很多资料之后找到一个解决方案。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值