本项目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
有的工具类会有初始化的动作,使用该注解可以阻止初始化动作,值为类的包含包路径的全路径。