junit4整合PowerMockito进行单元测试
一、介绍
在单元测试中,代码里面往往有一些需要连接数据库、调用第三方远程的代码。
由于没有环境,这些代码的存在,会给单元测试造成影响。
所以我们在单测中,往往会使用mock
的方式对这些代码做一个数据的模拟,从而达到对代码进行测试的一个目的。
所以单测需要满足以下几点
- 可复用:单测代码可以重复执行
- 无环境:不要依赖数据库,第三方接口等外部的环境依赖
- 方法级细粒度:单测代码应该针对具体一个方法的测试,
- 高覆盖率:如果代码中复杂度过高,单测要覆盖到方法中的每一行代码
- 自动断言:每一段单测代码都应该有自己的断言方法,而不是通过打印再人工查看正确性
所以我们就有了Mockito
,它可以模拟对象,模拟对象方法的返回值,来完成mock
。
本文使用的是PowerMockito
,它是由Mockito
的基础上开发而来,语法规则基本一致,同时也有一些自己的增强,可以对静态方法,局部变量进行mock
。
二、初步入门
假设我们有下面这两段代码PowerMockitoServiceImpl.java
和PowerMockitoMapper.java
package com.banmoon.test.service.impl;
import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import com.banmoon.test.service.PowerMockitoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {
@Autowired
private PowerMockitoMapper powerMockitoMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void insert(PowerMockitoEntity entity) {
Boolean status = Optional.ofNullable(entity.getValue()).map(a -> Boolean.TRUE).orElse(Boolean.FALSE);
entity.setStatus(status);
powerMockitoMapper.insert(entity);
}
}
代码语言:javascript复制package com.banmoon.test.mapper;
import com.banmoon.test.entity.PowerMockitoEntity;
public interface PowerMockitoMapper {
int insert(PowerMockitoEntity entity);
}
还有一段PowerMockitoEntity.java
package com.banmoon.test.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName("power_mockito")
public class PowerMockitoEntity {
@TableId
private Integer id;
private String value;
private Boolean status;
}
上面代码所做的功能就是,插入一个实体至数据库。
在插入前,我们根据entity.value
是否有值,给予entity.status
的值
故此,上面的代码需要连接数据库,我们在单测时,直接对PowerMockitoMapper
进行mock
即可
首先,先导入依赖,根据自己的需要进行删减使用
代码语言:javascript复制<!-- powermock -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4-rule</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-classloading-xstream</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
代码如下
代码语言:javascript复制package com.banmoon.test.service.impl;
import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.mockito.Mockito.when;
@RunWith(PowerMockRunner.class)
public class PowerMockitoServiceImplTest {
@Mock
private PowerMockitoMapper mockPowerMockitoMapper;
@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;
/**
* 有值测试
*/
@Test
public void testInsert1() {
// 设置参数
final PowerMockitoEntity entity = new PowerMockitoEntity();
entity.setId(1);
entity.setValue("有值测试");
when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);
// 执行测试
powerMockitoServiceImplUnderTest.insert(entity);
// 校验结果
Assert.assertTrue(entity.getStatus());
}
/**
* 无值测试
*/
@Test
public void testInsert2() {
// 设置参数
final PowerMockitoEntity entity = new PowerMockitoEntity();
entity.setId(1);
entity.setValue(null);
when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);
// 执行测试
powerMockitoServiceImplUnderTest.insert(entity);
// 校验结果
Assert.assertFalse(entity.getStatus());
}
}
执行结果如下,保证两个测试方法如预期通过即可
三、其他使用
1)如何对无返回值的方法进行断言
假设有一个无返回值的方法,我们要针对它进行测试。由于它没有返回值,就没有办法对其返回值进行断言校验。
那么针对这种情况,一个方法,就算是无返回值的情况。内部一定做了一些什么操作。所以我们一般有两种方式
- 这个方法做了设置某个对象的属性,我们可以校验对象属性是否符合预期
- 如第二章的初步使用就是如此
- 如果这个方法执行了某段逻辑分支的代码,我们可以可以校验那段代码是否执行过
PowerMockitoServiceImpl.java
上再加一个无返回值的方法
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {
@Autowired
private PowerMockitoMapper powerMockitoMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(PowerMockitoEntity entity) {
if (entity.getId() == null) {
powerMockitoMapper.insert(entity);
} else {
powerMockitoMapper.updateById(entity);
}
}
}
PowerMockitoMapper.java
如下
package com.banmoon.test.mapper;
import com.banmoon.test.entity.PowerMockitoEntity;
public interface PowerMockitoMapper {
int insert(PowerMockitoEntity entity);
int updateById(PowerMockitoEntity entity);
}
那么我们可以这样
代码语言:javascript复制package com.banmoon.test.service.impl;
import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.mockito.Mockito.when;
@RunWith(PowerMockRunner.class)
public class NoneReturnTest {
@Mock
private PowerMockitoMapper mockPowerMockitoMapper;
@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;
/**
* 有ID测试
*/
@Test
public void test1() {
// 设置参数
final PowerMockitoEntity entity = new PowerMockitoEntity();
entity.setId(1);
entity.setValue("测试");
when(mockPowerMockitoMapper.updateById(entity)).thenReturn(1);
// 执行测试
powerMockitoServiceImplUnderTest.saveOrUpdate(entity);
// 校验结果
Mockito.verify(mockPowerMockitoMapper).updateById(entity);
}
/**
* 没有ID测试
*/
@Test
public void test2() {
// 设置参数
final PowerMockitoEntity entity = new PowerMockitoEntity();
entity.setId(null);
entity.setValue("测试");
when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);
// 执行测试
powerMockitoServiceImplUnderTest.saveOrUpdate(entity);
// 校验结果
Mockito.verify(mockPowerMockitoMapper).insert(entity);
}
}
2)对属局部对象进行mock并设置
如果一个方法中,有一个自己实例化的一个局部变量,那么我们该如何对其进行mock
呢?
例如下面这个方法,有一个自己的局部变量tuple
,并返回了这个局部变量的数量
package com.banmoon.service.impl;
import com.banmoon.service.PowerMockitoService;
import org.springframework.stereotype.Service;
import java.io.File;
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {
@Override
public long localVariable() {
File file = new File("E://abc.txt");
return file.length();
}
}
我们只需要这样进行,即可以完成对局部变量的mock
package com.banmoon.test.powerMockitoTest;
import com.banmoon.service.impl.PowerMockitoServiceImpl;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.io.File;
@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class})
public class LocalVariableTest {
@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;
@Test
public void localVariableTest() throws Exception {
// 设置参数
File file = PowerMockito.mock(File.class);
// mock
PowerMockito.whenNew(File.class)
.withAnyArguments()
.thenReturn(file);
PowerMockito.when(file.length()).thenReturn(2L);
// 执行测试
long i = powerMockitoServiceImplUnderTest.localVariable();
// 校验结果
Assert.assertEquals(2L, i);
}
}
3)对静态方法mock
如何对静态方法的返回值进行mock
先在PowerMockitoServiceImpl.java
添加一个静态方法,其中发现HttpUtil.get()
是一个静态方法
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {
@Autowired
private PowerMockitoMapper powerMockitoMapper;
@Override
public int syncPowerMockitoEntity() {
String result = HttpUtil.get("url");
if (CharSequenceUtil.isNotBlank(result)) {
List<JSONObject> list = JSON.parseObject(result, List.class);
int i = 0;
for (JSONObject json : list) {
PowerMockitoEntity entity = json.toJavaObject(PowerMockitoEntity.class);
i = powerMockitoMapper.insert(entity);
}
return i;
} else {
throw new BanmoonException(1001, "同步出现异常");
}
}
}
针对上面的方法,我们可以这样进行mock
,注意@PrepareForTest
注解,一定要写上,改变了其中的字节码
package com.banmoon.test.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Assert;
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 java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest(HttpUtil.class)
public class StaticMethodTest {
@Mock
private PowerMockitoMapper mockPowerMockitoMapper;
@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;
/**
* 静态方法mock
*/
@Test
public void test() {
// 设置参数
final PowerMockitoEntity entity1 = new PowerMockitoEntity();
entity1.setId(1);
entity1.setValue("测试1");
final PowerMockitoEntity entity2 = new PowerMockitoEntity();
entity2.setId(2);
entity2.setValue("测试2");
List<PowerMockitoEntity> list = CollUtil.newArrayList(entity1, entity2);
// mock
PowerMockito.mockStatic(HttpUtil.class);
when(HttpUtil.get(any())).thenReturn(JSON.toJSONString(list));
when(mockPowerMockitoMapper.insert(any())).thenReturn(1);
// 执行测试
int i = powerMockitoServiceImplUnderTest.syncPowerMockitoEntity();
// 校验结果
Assert.assertEquals(2, i);
}
}
4)mock final修饰的类和方法
首先我们先写一个工具类,这个工具类是final
修饰的,里面的方法也是final
的
package com.banmoon.util;
import cn.hutool.core.util.RandomUtil;
public final class PowerMockitoUtil {
public final String finalMethod(int length) {
return RandomUtil.randomString(length);
}
}
单测这个方法
代码语言:javascript复制package com.banmoon.service.impl;
import com.banmoon.service.PowerMockitoService;
import com.banmoon.util.PowerMockitoUtil;
import org.springframework.stereotype.Service;
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {
private final PowerMockitoUtil powerMockitoUtil = new PowerMockitoUtil(10);
@Override
public int finalMethod() {
String method = powerMockitoUtil.finalMethod();
return method.length();
}
}
测试类
代码语言:javascript复制package com.banmoon.test.powerMockitoTest;
import com.banmoon.service.impl.PowerMockitoServiceImpl;
import com.banmoon.util.PowerMockitoUtil;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.lang.reflect.Field;
@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class, PowerMockitoUtil.class})
public class FinalMethodTest {
@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;
@Test
public void finalMethodTest() throws Exception {
// 设置参数
PowerMockitoUtil util = PowerMockito.mock(PowerMockitoUtil.class);
// mock
Field field = PowerMockito.field(PowerMockitoServiceImpl.class, "powerMockitoUtil");
field.set(powerMockitoServiceImplUnderTest, util);
PowerMockito.when(util.finalMethod()).thenReturn("abcde");
// 执行测试
int i = powerMockitoServiceImplUnderTest.finalMethod();
// 校验结果
Assert.assertEquals(5, i);
}
}
5)异常的情况
有些时候,代码是会发生异常的,那么在单测的环境下,我们需要判断这些异常是什么,是不是符合预期
如下这个方法,我们只需要传个null
,就会发生NullPointException
的异常
package com.banmoon.service.impl;
import com.banmoon.service.PowerMockitoService;
import org.springframework.stereotype.Service;
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {
@Override
public int exceptionMethod(String name) {
return name.length();
}
}
测试用例
代码语言:javascript复制package com.banmoon.test.powerMockitoTest;
import com.banmoon.service.impl.PowerMockitoServiceImpl;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class})
public class ExceptionMethodTest {
@InjectMocks
private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void exceptionMethodTest() throws Exception {
// 校验结果
Assert.assertThrows(NullPointerException.class, () -> {
// 执行测试
int i = powerMockitoServiceImplUnderTest.exceptionMethod(null);
});
}
@Test
public void exceptionMethodTest2() throws Exception {
// 校验结果
thrown.expect(NullPointerException.class);
// 执行测试
int i = powerMockitoServiceImplUnderTest.exceptionMethod(null);
}
}
四、最后
推荐一个很好用的IDEA
插件,这个插件可以快速生成单元测试代码
squaretest
我是半月,你我一同共勉!!!