Spring知识点学习(7):使用Mockito+Junit4框架编写测试用例

本文深入探讨PowerMock框架,解析其如何解决jMock、EasyMock、Mockito等工具的局限,尤其在静态、final及私有方法mock方面的优势。通过实战案例,涵盖依赖引入、静态方法、方法调用、构造方法、私有方法mock技巧,以及实体类和DTO的快速测试方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本项目GitHub地址:https://github.com/SirLiuGang/Spring/tree/master/spring-test


现如今比较流行的Mock工具如jMock 、EasyMock 、Mockito等都有一个共同的缺点:不能mock静态、final、私有方法等。而PowerMock能够完美的弥补以上三个Mock工具的不足。
PowerMock github wiki地址:https://github.com/powermock/powermock/wiki/Getting-Started

引入PowerMock依赖

参考官网介绍,有编写测试用例的书写方式和Maven依赖的引入方式:
在这里插入图片描述
点击JUnit中的Mockito2 Junit Maven setup,然后复制maven依赖。
在这里插入图片描述
在spring和mockito进行整合的时候,遇到了版本冲突的问题。
我在spring-pom.xml中是直接依赖了spring boot的test包,里边集成了Mockito的版本为2.23.4
在这里插入图片描述
在这里插入图片描述
官方wiki上的版本只显示到1.7.1
在这里插入图片描述
后来去Maven仓库查询到PowerMock版本需要为2.0.0,才能满足mockito2.23.0以上的版本。
在这里插入图片描述
完整的pom.xml配置如下:

	<properties>
        <powermock.version>2.0.0</powermock.version>
    </properties>

    <dependencies>
        <!-- powermock 的依赖 -->
        <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>

    </dependencies>

至此,准备工作就做好了。

mock静态方法

首先写一个类,里边包含静态方法和普通的方法,然后在普通方法里边调用静态方法:

public class StaticUtils {
    /**
     * 获取传入的数字
     */
    public static Integer getNum(Integer integer) {
        return integer;
    }

    /**
     * 判断传入的参数是否等于0
     * @param num 传入的参数
     * @return  true  入参 == 0
     *          false 入参 != 0
     */
    public boolean isZero(Integer num) {
        return StaticUtils.getNum(num) == 0;
    }
}

现在要对getNum静态方法进行模拟,首先建一个测试类,类名为Test+需要测试的类:

  • @RunWith(PowerMockRunner.class) 如果要模拟静态方法,这个必须有
  • @PrepareForTest(StaticUtils.class) 将需要模拟的静态方法属于的类在这里准备
  • MockitoAnnotations.initMocks(this) 初始化mock注解
@RunWith(PowerMockRunner.class)
@PrepareForTest(StaticUtils.class)
public class TestStaticUtils {

    private static Logger LOG = LoggerFactory.getLogger(TestStaticUtils.class);

    private StaticUtils staticUtils = new StaticUtils();

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

    /**
     * 未mock静态方法
     */
    @Test
    public void isZero_noMock() {
        boolean result = staticUtils.isZero(0);
        
        assertTrue(result);
        
        LOG.info("result = {}", result);		// result = true
    }

    /**
     * 使mock静态方法
     */
    @Test
    public void isZero_mock() {
        PowerMockito.mockStatic(StaticUtils.class);
        // 模拟静态方法,当入参为0的时候,输出1
        Mockito.when(StaticUtils.getNum(0)).thenReturn(1);
        boolean result = staticUtils.isZero(0);
        
        assertFalse(result);
        
        LOG.info("result = {}", result);		// result = false
    }

}

至此,成功对静态方法进行了mock。

mock方法调用

先在spring-pom中引入web开发的依赖:

		<dependency>
            <!-- spring-boot-start-web : MVC,AOP的依赖包。。。 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

再写个Controller层和Service层和Dao层

@Controller
@RequestMapping(value = "/user")
public class UserController {

    @Resource(name = "userService")
    private IUserService userService;

    @RequestMapping(value = "/getUserById", method = RequestMethod.GET)
    @ResponseBody
    public User getUserById(Long userId) {
        return userService.getById(userId);
    }
}
@Service(value = "userService")
public class UserServiceImpl implements IUserService {

    @Resource(name = "userDao")
    private IUserDao userDao;

    @Override
    public User getById(Long userId) {
        return userDao.getById(userId);
    }
}
@Service(value = "userDao")
public class UserDaoImpl implements IUserDao {
    @Override
    public User getById(Long userId) {
        return new User(userId, "张三", "男");
    }
}

直接启动Application,然后打开浏览器调用一下,说明可以使用:
在这里插入图片描述
现在开始写测试用例:

  • RunWith这个无所谓,主要是这里是springboot的项目,所以使用SpringRunner进行跑测试用例
  • @InjectMocks必须要加,这个注解不会把一个类变成mock或是spy,但是会把当前对象下面的Mock/Spy类注入进去,按类型注入。如果不加的话userService就没办法注入了。@Spy为监视真实的对象
  • @Before注解需要加,由于使用的是Junit4框架,所以需要初始化mock注解,不然没法使用。
  • 有了上边的准备,@Mock private IUserService userService 就可以在Controller层调用的时候自动注入了,然后可以自己设定返回值等,满足自己的测试需求,从而不需要使用真实的数据。
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestUserController {

    private static final Long USERID = 123L;

    // 引入要测试的Controller
    @InjectMocks    // 这个注解不会把一个类变成mock或是spy,但是会把当前对象下面的Mock/Spy类注入进去,按类型注入。
    @Spy
    private UserController userController = new UserController();

    @Mock
    private IUserService userService;

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

    /**
     * 没有对Service方法进行mock
     */
    @Test
    public void getUserById_noMock() {
        User user = userController.getUserById(USERID);
        // 返回结果为null
        assertNull(user);
    }

    /**
     * 对Service方法进行mock
     */
    @Test
    public void getUserById_mock() {
        User user = new User(1L, "李四", "男");
        // 模拟service方法的返回值
        PowerMockito.doReturn(user).when(userService).getById(USERID);
        User resutnUser = userController.getUserById(USERID);

        // 看调用Controller层的接口后返回值和定义的是否是同一个对象
        assertEquals(user, resutnUser);
    }
}

mock构造方法

有时候如果在类中进行new了实体类,那么只能通过PowerMock进行模拟,否则测试用例无法覆盖全面。

Dao层代码:

@Service(value = "userDao")
public class UserDaoImpl implements IUserDao {
    @Override
    public User getById(Long userId) {
        return new User(userId, "张三", "男");
    }
}

给Dao层写测试用例:

  • @RunWith(PowerMockRunner.class) 是必须的,模拟构造方法需要用该方法跑
  • PowerMockito.whenNew(User.class).withAnyArguments().thenReturn(expectUser); 当对User进行new 的时候,不论有多少参数,均返回expectUser。
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserDaoImpl.class)
public class TestUserDaoImpl {

    private static final Long USERID = 123L;

    @InjectMocks
    @Spy
    private UserDaoImpl userDao = new UserDaoImpl();

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


    /**
     * 不mock返回结果
     */
    @Test
    public void getById_noMock() {
        // 重写了User的equals方法
        User user = new User(USERID, "张三", "男");
        User resultUser = userDao.getById(USERID);

        // 返回结果和期望的返回结果是相同的
        Assert.assertEquals(user, resultUser);
    }

    /**
     * mock返回结果
     */
    @Test
    public void getById_mock() {
        User user = new User(USERID, "张三", "男");

        User expectUser = new User(USERID, "李四", "男");
        try {
            // 当方法中new User 的时候,都会返回我们自定义的expectUser对象,而不是方法中内容
            PowerMockito.whenNew(User.class).withAnyArguments().thenReturn(expectUser);
        } catch (Exception e) {
            e.printStackTrace();
        }

        User resultUser = userDao.getById(USERID);

        // 返回结果和修改前的期望结果是不同的
        Assert.assertNotEquals(user, resultUser);
    }
}

由于需要对两个对象进行比较,所以需要重写User实体类的equals方法:

	@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
                Objects.equals(name, user.name) &&
                Objects.equals(sex, user.sex);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, sex);
    }

mock私有方法

创建私有方法:

public class PrivateMethod {

    /**
     * 进行字符串的拼接
     */
    public String getStr(String str) {
        return "123" + appendABC(str);
    }

    /**
     * 私有方法,在字符串后边拼接字母  ABC
     */
    private String appendABC(String str) {
        return str + "ABC";
    }
}

编写测试用例:

  • 由于是对私有方法进行mock,所以需要@RunWith(PowerMockRunner.class)
  • 另外是对方法的模拟,所以需要@PrepareForTest(PrivateMethod.class)
  • @Spy也是必须的
  • PowerMockito.when(privateMethod, “appendABC”, any()).thenReturn("__"); 当调用appendABC,并且参数为any(任何参数)的时候,返回三个“”,这样就与期待的123abcABC不同了
@RunWith(PowerMockRunner.class)
@PrepareForTest(PrivateMethod.class)
public class TestPrivateMethod {

    @InjectMocks
    @Spy
    private PrivateMethod privateMethod = new PrivateMethod();

    /**
     * 未mock私有方法
     */
    @Test
    public void getStr_noMock() {
        String exceptStr = "123abcABC";

        String resultStr = privateMethod.getStr("abc");

        Assert.assertEquals(exceptStr, resultStr);
    }

    /**
     * 对私有方法进行mock
     */
    @Test
    public void getStr_mock() {
        String exceptStr = "123abcABC";

        try {
        	// 第一个参数:需要mock的类,第二个参数:方法的名称,第三个参数:调用方法入参格式
            PowerMockito.when(privateMethod, "appendABC", any()).thenReturn("___");
        } catch (Exception e) {
            e.printStackTrace();
        }

        String resultStr = privateMethod.getStr("abc");

        Assert.assertNotEquals(exceptStr, resultStr);
    }
}

快速对实体类和DTO进行测试

从网上拷贝的代码,改了改,对实体类进行实例化,然后调用set和get方法,还有toString,然后把需要进行测试的类放进去即可,还有个扫描包下边所有的实体类的功能,等以后有时间了再写。

代码地址:https://github.com/SirLiuGang/Spring/blob/master/spring-test/src/test/java/com/cn/lg/test/entity/TestReflection.java

被测试类:

package com.cn.lg.test.entity;

import java.io.Serializable;
import java.util.Objects;

/**
 * 用户实体类
 * @Auther: 刘钢
 * @Date: 2019/3/2 23:55
 * @Description:
 */
public class User implements Serializable {

    private static final long serialVersionUID = 779518488091185603L;

    private Long id;
    private String name;
    private String sex;

    public User() {
    }

    public User(Long id, String name, String sex) {
        this.id = id;
        this.name = name;
        this.sex = sex;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
                Objects.equals(name, user.name) &&
                Objects.equals(sex, user.sex);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, sex);
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }
}

利用反射进行测试:

package com.cn.lg.test.entity;

import org.junit.Test;

import java.lang.reflect.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

/**
 * 利用反射完成对实体类或DTO的测试
 * @Auther: 刘钢
 * @Date: 2019/3/4 22:36
 * @Description:
 */
public class TestReflection {

    /**
     * 获取所有需要测试的Class
     */
    private List<Class<?>> getClasses() {
        List<Class<?>> list = new ArrayList<>();
        list.add(User.class);   // 对User实体类进行测试
        return list;
    }

    /**
     * 利用反射完成对实体类或DTO的测试
     */
    @Test
    public void test() {

        List<Class<?>> allClass = getClasses();
        if (null != allClass) {
            for (Class classes : allClass) {// 循环反射执行所有类
                try {
                    boolean isAbstract = Modifier.isAbstract(classes.getModifiers());
                    if (classes.isInterface() || isAbstract) {// 如果是接口或抽象类,跳过
                        continue;
                    }
                    Constructor[] constructorArr = classes.getConstructors();
                    Object clazzObj = newConstructor(constructorArr, classes);

                    fieldTest(classes, clazzObj);

                    methodInvoke(classes, clazzObj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 对属性进行测试
     */
    private void fieldTest(Class<?> classes, Object clazzObj) throws Exception {
        if (null == clazzObj) {
            return;
        }

        Field[] fields = classes.getDeclaredFields();
        if (null != fields && fields.length > 0) {
            for (Field field : fields) {
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                Object fieldGetObj = field.get(clazzObj);
                if (!Modifier.isFinal(field.getModifiers()) || null == fieldGetObj) {
                    field.set(clazzObj, adaptorGenObj(field.getType()));
                }
            }
        }
    }

    /**
     * 功能描述: 执行方法<br>
     */
    private void methodInvoke(Class<?> classes, Object clazzObj) throws Exception {
        Method[] methods = classes.getDeclaredMethods();
        if (null != methods && methods.length > 0) {
            for (Method method : methods) {
                String methodName = method.getName();

                // 无论如何,先把权限放开
                method.setAccessible(true);
                Class<?>[] paramClassArrs = method.getParameterTypes();

                // 执行getset方法
                if (methodName.startsWith("set") && null != clazzObj) {
                    methodInvokeGetSet(classes, clazzObj, method, paramClassArrs, methodName);
                    continue;
                }

                // 如果是静态方法
                if (Modifier.isStatic(method.getModifiers()) && !classes.isEnum()) {
                    if (paramClassArrs.length == 0) {
                        method.invoke(null);
                    } else if (paramClassArrs.length == 1) {
                        method.invoke(null, adaptorGenObj(paramClassArrs[0]));
                    } else if (paramClassArrs.length == 2) {
                        method.invoke(null, adaptorGenObj(paramClassArrs[0]), adaptorGenObj(paramClassArrs[1]));
                    } else if (paramClassArrs.length == 3) {
                        method.invoke(null, adaptorGenObj(paramClassArrs[0]), adaptorGenObj(paramClassArrs[1]),
                                adaptorGenObj(paramClassArrs[2]));
                    } else if (paramClassArrs.length == 4) {
                        method.invoke(null, adaptorGenObj(paramClassArrs[0]), adaptorGenObj(paramClassArrs[1]),
                                adaptorGenObj(paramClassArrs[2]), adaptorGenObj(paramClassArrs[3]));
                    }
                    continue;
                }

                if (null == clazzObj) {
                    continue;
                }

                // 如果方法是toString,直接执行
                if ("toString".equals(methodName)) {
                    try {
                        Method toStringMethod = classes.getDeclaredMethod(methodName);
                        toStringMethod.invoke(clazzObj);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    continue;
                }

                // 其他方法
                if (paramClassArrs.length == 0) {
                    method.invoke(clazzObj);
                } else if (paramClassArrs.length == 1) {
                    method.invoke(clazzObj, adaptorGenObj(paramClassArrs[0]));
                } else if (paramClassArrs.length == 2) {
                    method.invoke(clazzObj, adaptorGenObj(paramClassArrs[0]), adaptorGenObj(paramClassArrs[1]));
                } else if (paramClassArrs.length == 3) {
                    method.invoke(clazzObj, adaptorGenObj(paramClassArrs[0]), adaptorGenObj(paramClassArrs[1]),
                            adaptorGenObj(paramClassArrs[2]));
                } else if (paramClassArrs.length == 4) {
                    method.invoke(clazzObj, adaptorGenObj(paramClassArrs[0]), adaptorGenObj(paramClassArrs[1]),
                            adaptorGenObj(paramClassArrs[2]), adaptorGenObj(paramClassArrs[3]));
                }
            }
        }
    }

    /**
     * 功能描述: 执行getset方法,先执行set,获取set执行get<br>
     */
    private void methodInvokeGetSet(Class<?> classes, Object clazzObj, Method method, Class<?>[] paramClassArrs, String methodName)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Object getObj;
        String methodNameSuffix = methodName.substring(3);
        Method getMethod;
        try {
            getMethod = classes.getDeclaredMethod("get" + methodNameSuffix);
        } catch (NoSuchMethodException e) {
            // 如果对应的get方法找不到,会有is开头的属性名,其get方法就是其属性名称
            Character firstChar = methodNameSuffix.charAt(0);// 取出第一个字符转小写
            String firstLowerStr = firstChar.toString().toLowerCase();
            try {
                getMethod = classes.getDeclaredMethod(firstLowerStr + methodNameSuffix.substring(1));
            } catch (NoSuchMethodException e2) {
                return;
            }
        }

        // 如果get返回结果和set参数结果一样,才可以执行,否则不可以执行
        Class<?> returnClass = getMethod.getReturnType();
        if (paramClassArrs.length == 1 && paramClassArrs[0].toString().equals(returnClass.toString())) {
            getObj = getMethod.invoke(clazzObj);
            method.invoke(clazzObj, getObj);
        }

    }

    /**
     * 功能描述: 构造函数构造对象<br>
     */
    @SuppressWarnings("rawtypes")
    private Object newConstructor(Constructor[] constructorArr, Class<?> classes) throws Exception {
        if (null == constructorArr || constructorArr.length < 1) {
            return null;
        }
        Object clazzObj = null;
        boolean isExitNoParamConstruct = false;
        for (Constructor constructor : constructorArr) {
            Class[] constructParamClazzArr = constructor.getParameterTypes();
            if (constructParamClazzArr.length == 0) {
                constructor.setAccessible(true);
                clazzObj = classes.newInstance();
                isExitNoParamConstruct = true;
                break;
            }
        }
        // 没有无参构造取第一个
        if (!isExitNoParamConstruct) {
            boolean isContinueFor = false;
            Class[] constructParamClazzArr = constructorArr[0].getParameterTypes();
            Object[] construParamObjArr = new Object[constructParamClazzArr.length];
            for (int i = 0; i < constructParamClazzArr.length; i++) {
                Class constructParamClazz = constructParamClazzArr[i];
                construParamObjArr[i] = adaptorGenObj(constructParamClazz);
                if (null == construParamObjArr[i]) {
                    isContinueFor = true;
                }
            }
            if (!isContinueFor) {
                clazzObj = constructorArr[0].newInstance(construParamObjArr);
            }
        }
        return clazzObj;
    }

    /**
     * 功能描述: 根据类的不同,进行不同实例化<br>
     */
    private Object adaptorGenObj(Class<?> clazz) throws Exception {
        if (null == clazz) {
            return null;
        }
        if ("int".equals(clazz.getName())) {
            return 1;
        } else if ("char".equals(clazz.getName())) {
            return 'x';
        } else if ("boolean".equals(clazz.getName())) {
            return true;
        } else if ("double".equals(clazz.getName())) {
            return 1.0;
        } else if ("float".equals(clazz.getName())) {
            return 1.0f;
        } else if ("long".equals(clazz.getName())) {
            return 1L;
        } else if ("byte".equals(clazz.getName())) {
            return 0xFFFFFFFF;
        } else if ("java.lang.Class".equals(clazz.getName())) {
            return this.getClass();
        } else if ("java.math.BigDecimal".equals(clazz.getName())) {
            return new BigDecimal(1);
        } else if ("java.lang.String".equals(clazz.getName())) {
            return "333";
        } else if ("java.util.Hashtable".equals(clazz.getName())) {
            return new Hashtable();
        } else if ("java.util.Hashtable".equals(clazz.getName())) {
            return new Hashtable();
        } else if ("java.util.List".equals(clazz.getName())) {
            return new ArrayList();
        } else {
            // 如果是接口或抽象类,直接跳过
            boolean paramIsAbstract = Modifier.isAbstract(clazz.getModifiers());
            boolean paramIsInterface = Modifier.isInterface(clazz.getModifiers());
            if (paramIsInterface || paramIsAbstract) {
                return null;
            }
            Constructor<?>[] constructorArrs = clazz.getConstructors();
            return newConstructor(constructorArrs, clazz);
        }
    }

}

跑测试用例的时候用覆盖率去跑:
在这里插入图片描述
打开实体类,左侧绿色为覆盖到的代码,红色为未覆盖的,可以看到大部分还是被覆盖了,其他的重写的没有进行覆盖:
在这里插入图片描述
在左侧可以看到覆盖情况:
在这里插入图片描述
起码省去了很多写测试用例的时间。

mock时忽略不必要的初始化

@SuppressStaticInitializationFor
有的工具类会有初始化的动作,使用该注解可以阻止初始化动作,值为类的包含包路径的全路径。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

刘了了

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

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

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

打赏作者

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

抵扣说明:

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

余额充值