一、maven依赖
有些数据mock需要额外的工具进行(比如需要mock静态、final方法),如Mockito.MockedStatic,PowerMock等等,文档下面的例子全部都是使用PowerMock进行
maven依赖:
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.7.7</version>
<scope>test</scope>
</dependency>
其中mockito与PowerMock版本依赖关系为:
二、mock小技巧
1. mock静态方法
为什么mockito无法mock静态方法?
Mockito使用继承的方式实现mock的,用CGLIB生成mock对象代替真实的对象进行执行,为了mock实例的方法,可以在subclass中覆盖它,而static方法是不能被子类覆盖的,所以Mockito不能mock静态方法。
PowerMock如何解决这个问题?
PowerMock有着两个非常重要的依赖,一个是javassist,另外一个就是objenesis。其中javassist是一个修改java字节码的工具包;objenesis是一个绕过构造方法来实例化一个对象的工具包。PowerMock的本质是通过修改字节码来实现对静态和final等方法的mock的。
在某一个测试方法被注解@PrepareForTest标注之后,在运行测试用例的时候,会创建一个新的org.powermock.core.classloader.MockClassLoader实例,之后,加载这个测试用例使用到的类,注意这里的话,系统类要除外。PowerMock会依据mock要求,对在注解@PrepareForTest里的class文件进行修改(测试类会自己主动的加入注解当中),以此来满足特殊的mock需求。比如说去除final方法的final标识,在静态方法的最前面加入自己的虚拟实现等等。假如,需要mock的是系统类的final方法和静态方法,PowerMock不能直接修改系统类的class文件,而是去修改调用系统类的class文件,以此来满足mock需求。
使用powerMock步骤:
(1)在被测试类上加上注解:@PrepareForTest(Static.class)
(2)mock:PowerMockito.mockStatic(Static.class);
(3)Mockito.when(Static.firstStaticMethod(param)).thenReturn(value);
被测试类
public class StaticClass {
public static String str(){
return "aaa";
}
}
测试用例
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*","javax.script.*"})
@PrepareForTest({StaticClass.class})
public class MockStatic {
@Test
public void testStaticClass(){
PowerMockito.mockStatic(StaticClass.class);
PowerMockito.when(StaticClass.str()).thenReturn("bb");
Assert.assertNotEquals(StaticClass.str(),"aaa");
}
}
mock带参数的静态方法:
completableFutureMockedStatic.when(() -> CompletableFuture.runAsync(Mockito.any(),Mockito.any())).thenReturn(new CompletableFuture<>());
2. mock私有方法
被测试类
public class PrivateClass {
public String str(String ss){
return privateStr()+ss;
}
private String privateStr(){
return "aaa";
}
}
测试用例
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*","javax.script.*"})
@PrepareForTest({PrivateClass.class})
public class MockPrivate {
@InjectMocks
private PrivateClass privateClass;
@Test
public void testPrivate() throws Exception {
PrivateClass spy = PowerMockito.spy(privateClass);
// mock本地方法
PowerMockito.doReturn("qqq").when(spy,"privateStr");
Assert.assertEquals(spy.str("a"),"qqqa");
}
}
3. mock final方法
被测试类
public final class FinalClass {
public final String str() {
return "aaa";
}
}
测试用例
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*","javax.script.*"})
@PrepareForTest({FinalClass.class})
public class MockFinal {
@Test
public void testFinal(){
FinalClass mock = PowerMockito.mock(FinalClass.class);
PowerMockito.when(mock.str()).thenReturn("qqq");
Assert.assertEquals(mock.str(),"qqq");
}
}
4. mock 线程池
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*","javax.script.*"})
public class MockAsync {
@InjectMocks
private AsyncClass asyncClass;
@Mock
private ExecutorService executorService;
@Test
public void asyncTest() throws ExecutionException, InterruptedException {
Future<String> future = new Future<String>() {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return false;
}
@Override
public String get() throws InterruptedException, ExecutionException {
return "sss";
}
@Override
public String get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return null;
}
};
//数据mock
c
// 实际方法调用
String s = asyncClass.doInvoke();
Assert.assertEquals(s,"sss");
}
}
5. Apollo Mock
方式一:直接mock掉Apollo的静态方法,使其使用本地配置方法
@BeforeClass
public static void mockApollo(){
PowerMockito.mockStatic(EnvUtils.class);
when(EnvUtils.transformEnv(any())).thenReturn(Env.LOCAL);
}
这种方式的运行原理本质上是mock掉源码的调用流程,使其直接使用本地配置
方式二:修改环境属性
可以通过设置环境属性 apollo.env=Local (注意大小写)
在调用阿波罗配置的时候,不会去远程加载,直接本地使用默认值
如:
ConfigService.getAppConfig().getProperty("key","defaultValue")
直接获取到的值为“defaultValue”
但是有时候我们不仅仅只是使用apollo配置中心的默认值,还需要对配置中心返回值进行配置,如果执行测试用例时,不希望使用代码中的默认值,希望使用自己定义的配置值,参考链接中的配置方式。
public class TestBase {
@BeforeClass
public static void initApolloLocalConfig(){
/**
* 初始化本地化 阿波罗配置中心,不进行网络请求
* 在调用阿波罗配置的时候,就会使用默认值
* 如:
* ConfigService.getAppConfig().getProperty("key","defaultValue")
* 直接获取到的值为“defaultValue”
*/
System.setProperty("apollo.env","Local");
}
}
注:建议将以上几种方式都写在@BeforeClass标注的方法中,如果写在@Before标注的方法中,可能存在以下几种问题:
1. 每次运行一个测试用例,都会执行@Before方法,实际上是没有必要的
2. 如果被测试类在属性初始化或者静态方法中使用了Apollo配置,@Before无法生效
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*","javax.script.*"})
public class ThreadServiceTest {
// 测试类运行时,会初始化ThreadService,然后将@Mock标注的属性注入到ThreadService 中,在初始化中如果有静态方法用到了Apollo配置或者像例子中实例化一个属性用到Apollo配置都会导致再次拉取Apollo
@InjectMocks
private ThreadService threadService;
@Mock
private ThreadDetailInterveneCache threadDetailInterveneCache;
@Mock
private ThreadRecommendService threadRecommendService;
@Mock
private ThreadParser threadParser;
@Before
public static void baseMockApollo(){
System.setProperty("apollo.env","Local");
}
}
------------------------------------------------------
@Service
public class ThreadService {
private ExecutorService batchGetThreadExecutorService = new TraceExecutorService(new ThreadPoolExecutor(getCoreThreadNum(), getMaxThreadNum(), getThreadAliveTime(), TimeUnit.SECONDS,
new LinkedBlockingQueue<>(getQueueCapacity()), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()));
private static int getThreadAliveTime() {
return ConfigService.getAppConfig().getIntProperty("tribe.batch.get.thread.time", 120);
}
private static int getCoreThreadNum() {
return ConfigService.getAppConfig().getIntProperty("tribe.batch.get.thread.core.thread.num", 16);
}
private static int getQueueCapacity() {
return ConfigService.getAppConfig().getIntProperty("tribe.batch.get.thread.queue.capacity", 1000);
}
private static int getMaxThreadNum() {
return ConfigService.getAppConfig().getIntProperty("tribe.batch.get.thread.max.num", 16);
}
}
但是有时候我们不仅仅只是使用apollo配置中心的默认值,还需要对配置中心返回值进行配置,按自定义的配置值来跑用例
第一步:在设置环境属性
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*","javax.script.*"})
public class DemoTest extends TestBase {
// 该单元测试类中需要自定义的配置内容, 可以直接通过原来的方式获取到该内容
@Before
public void initApolloConfig(){
System.setProperty("对应阿波罗配置中心的key","阿波罗配置中心的value");
System.setProperty("对应阿波罗配置中心的key","阿波罗配置中心的value");
System.setProperty("对应阿波罗配置中心的key","阿波罗配置中心的value");
}
@Test
public void testMethod(){
// do test things
}
}
方式三:mock apollo
//mock Apollo
Config application = mock(Config.class);
when(application.getIntProperty(eq("splash.batch.getBuoy.left.time"), anyInt())).thenReturn(500);
PowerMockito.mockStatic(ConfigService.class);
when(ConfigService.getAppConfig()).thenReturn(application);
6. mock Dubbo服务调用
1. mock dubbo服务的异步调用
dubbo服务调用实质也是对于静态方法的mock
被测试类
@Service
public class MessageEngineService {
private static final Logger logger = LoggerFactory.getLogger(MessageEngineService.class);
@Autowired
private MessageEngineFacade messageEngineFacade;
/**
* 创建消息主体内容
**/
public MessageContentDto createMessageContent(MessageContentDto messageContentDto){
if(messageContentDto == null){
logger.warn("MessageEngineService.createMessageContent param is empty. messageContentDto:{}", JSON.toJSONString(messageContentDto));
return null;
}
try {
messageEngineFacade.createMessageContent(messageContentDto);
CompletableFuture<ResultDto<MessageContentDto>> future = RpcContext.getContext().getCompletableFuture();
ResultDto<MessageContentDto> result = future.get();
if(result.isSuccess()){
return result.getT();
}
logger.info("createMessageContent result failed. messageContentDto:{}",JSON.toJSONString(messageContentDto));
return null;
}catch (Exception e){
logger.error("MessageEngineService.createMessageContent error,messageContentDto:{},e={}", JSON.toJSONString(messageContentDto), e);
return null;
}
}
}
测试类
@PrepareForTest({RpcContext.class})
public class MessageEngineServiceTest extends BaseMock {
@InjectMocks
private MessageEngineService messageEngineService;
@Mock
private RpcContext rpcContext;
@Mock
private MessageEngineFacade messageEngineFacade;
@Test
public void createMessageContent() throws ExecutionException, InterruptedException {
PowerMockito.doReturn(null).when(messageEngineFacade).createMessageContent(Mockito.any());
// mock 异步调用
PowerMockito.mockStatic(RpcContext.class);
PowerMockito.when(RpcContext.getContext()).thenReturn(rpcContext);
MessageContentDto messageContentDto = new MessageContentDto();
messageContentDto.setBody("aaa");
ResultDto resultDto = new ResultDto();
resultDto.setT(messageContentDto);
resultDto.setSuccess(true);
PowerMockito.doReturn(buildCompletableFuture(resultDto)).when(rpcContext).getCompletableFuture();
MessageContentDto messageContent = messageEngineService.createMessageContent(new MessageContentDto());
Assert.assertEquals(messageContent.getBody(),"aaa");
}
private <T> CompletableFuture<T> buildCompletableFuture(T value){
return CompletableFuture.completedFuture(value);
}
}
示例二:
private AppCatInfo appRemote(Long appId) {
AppCatInfo info = null;
try {
appCatService.queryAppCatInfo(null, appId);
if (info == null) {
Future<AppCatInfo> future = RpcContext.getContext().getFuture();
info = future.get();
}
} catch (Exception e) {
logger.error("获取资源信息出错, appId:{}", appId, e);
}
return info;
}
测试类:
import org.apache.dubbo.rpc.RpcContext;
import org.junit.Assert;
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.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
@RunWith(PowerMockRunner.class)
@PrepareForTest({RpcContext.class})
@PowerMockIgnore({"javax.management.*", "javax.script.*"})
public class CatManagerTest extends BaseJunitCase {
@InjectMocks
private CatManager catManager;
@Mock
private RpcContext rpcContext;
@Mock
private AppCatService appCatService;
@Before
public void init() throws Exception {
PowerMockito.doReturn(null).when(appCatService).queryAppCatInfo(any(), anyLong());
// mock 异步调用
PowerMockito.mockStatic(RpcContext.class);
PowerMockito.when(RpcContext.getContext()).thenReturn(rpcContext);
PowerMockito.doReturn(buildFuture(builderAppCatInfo())).when(rpcContext).getFuture();
}
@Test
public void testAppRemote() throws Exception {
AppCatInfo appCatInfo = Whitebox.invokeMethod(catManager, "appRemote", 1L);
System.out.println(appCatInfo);
Assert.assertEquals(1L, appCatInfo.getAppId());
}
private <T> Future<T> buildFuture(T value){
return CompletableFuture.completedFuture(value);
}
private AppCatInfo builderAppCatInfo() {
AppCatInfo appCatInfo = new AppCatInfo();
appCatInfo.setAppId(1L);
return appCatInfo;
}
}
2. mock dubbo服务的同步调用
同步调用时跟一般mock方式是一样的
@Test
public void syncCreateMessageContentTest(){
MessageContentDto messageContentDto = new MessageContentDto();
messageContentDto.setBody("aaa");
ResultDto resultDto = new ResultDto();
resultDto.setT(messageContentDto);
resultDto.setSuccess(true);
PowerMockito.doReturn(resultDto).when(messageEngineFacade).createMessageContent(Mockito.any());
MessageContentDto messageContent = messageEngineService.syncCreateMessageContent(new MessageContentDto());
Assert.assertEquals(messageContent.getBody(),"aaa");
}
7. 匹配对象参数
应用场景
public void provideColumnAccountDtos(String userId, String sender) {
// 请求参数由程序内部自己new出来的
MessageHomeReq messageHomeReq = new MessageHomeReq();
messageHomeReq.setSenders(Lists.newArrayList(sender));
messageHomeReq.setUserId(userId);
List<MessageHomeDto> messageHomeDtos = messageEngineService.messageHome(messageHomeReq);
}
方式一:参数匹配器(any)
PowerMockito.when(messageEngineService.messageHome(Mockito.any())).thenReturn(value);
// 被测试方法内多次调用mock方法,第一次返回value1,第二次返回value2
PowerMockito.when(messageEngineService.messageHome(Mockito.any())).thenReturn(value1, value2);
方式二:参数匹配器(eq)
PowerMockito.doReturn(Lists.newArrayList(messageHomeDto)).when(messageEngineService).messageHome(Mockito.eq(messageHomeReq));
注:如果是复杂对象,使用eq时需要参数对象实现equals、hashCode方法
8.mapStruct 与 mockito
通过mapStruct 编写的dto转换类实质上还是一种工具类,但为了使用方便,在项目中通常会通过spring的方式进行依赖注入
如果我们希望测试的时候不用mock这种dto转换类数据,直接走实际流程,怎么操作呢?
9.mock post请求
@RunWith(PowerMockRunner.class)
@PrepareForTest(ConfigService.class)
@PowerMockIgnore({"javax.management.*"})
public class RiskUtilTest {
private RiskUtil riskUtil = new RiskUtil();
@Before
public void init() throws Exception {
HttpClient httpClient = mock(HttpClient.class);
when(httpClient.post(anyString(), anyMap(), anyString(), any())).thenReturn(getResult());
ReflectionTestUtils.setField(riskUtil, "httpClient", httpClient, HttpClient.class);
}
private String getResult() {
// return "{\"result\":{\"score\":90.0,\"riskLevel\":\"DANGER\",\"firedRules\":[\"SSOID_GLOBAL_LIST\"],\"explanations\":[\"全局风险名单\"],\"rulePoint\":\"SSOID\"},\"suc\":true,\"code\":1001,\"message\":\"\"}";
// return "{\"result\":{\"score\":79.0,\"riskLevel\":\"HIGH\",\"firedRules\":[\"SSOID_GLOBAL_LIST\"],\"explanations\":[\"全局风险名单\"],\"rulePoint\":\"SSOID\"},\"suc\":true,\"code\":1001,\"message\":\"\"}";
return "{\"result\":{\"score\":0.0,\"riskLevel\":\"LOW\",\"firedRules\":[],\"explanations\":[\"\"],\"rulePoint\":\"\"},\"suc\":true,\"code\":1001,\"message\":\"\"}";
}
@Test
public void testRiskReport() {
RiskConfig riskConfig = mockRiskConfig(RiskUtil.ADD_POINT);
System.out.println("风控配置文件:" + JSON.toJSONString(riskConfig));
//获取风控结果
System.out.println("风控返回结果:" + riskUtil.getRiskReport(mockPageRequest(), riskConfig));
}
private RiskConfig mockRiskConfig(int type) {
String riskConfigStr = Constants.getRiskConfig();
List<RiskConfig> riskConfigList = JSONObject.parseArray(riskConfigStr, RiskConfig.class);
Map<Integer,RiskConfig> tempMap = riskConfigList.stream().collect(Collectors.toMap(RiskConfig::getType, Function.identity(),(key1, key2)->key2));
return tempMap.get(type);
}
}
三、编写测试用例小知识点
1.参数化测试
(1)Junit4 参数化测试
官方文档:参数化测试
步骤:
- 在测试类上加上注解@RunWith(Parameterized.class)
- 构造参数
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }
});
}
3.参数注入
3.1 通过构造函数注入
@RunWith(Parameterized.class)
public class MockParams1 {
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }
});
}
private int value1;
private int value2;
public MockParams1(int value1, int value2) {
this.value1 = value1;
this.value2 = value2;
}
@Test
public void test(){
System.out.println(value1 + value2);
}
}
3.2 通过参数注入
@RunWith(Parameterized.class)
public class MockParams {
// 需要是public static修饰
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }
});
}
// public
@Parameterized.Parameter
public int value1;
@Parameterized.Parameter(1)
public int value2;
@Test
public void test(){
System.out.println(value1 + value2);
}
}
(2)Junit5参数化测试
Junit5的参数化测试功能更加强大,可以支持枚举、方法、csv文件作为数据源
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
assertTrue(argument > 0 && argument < 4);
}
2.thenReturn与doReturn
在Mockito中打桩(即stub)有两种方法when(...).thenReturn(...)和doReturn(...).when(...)。这两个方法在大部分情况下都是可以相互替换的,但是在使用了Spies对象(@Spy注解),而不是mock对象(@Mock注解)的情况下他们调用的结果是不相同的
● when(...) thenReturn(...)会调用真实的方法,如果你不想调用真实的方法而是想要mock的话,就不要使用这个方法。
● doReturn(...) when(...) 不会调用真实方法
3.参数匹配器
使用参数匹配器时,所有参数都应使用匹配器。
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// 不正确的,第三个参数没有使用参数匹配器
verify(mock).someMethod(anyInt(), anyString(), "third argument");
如果参数是null值,需要用mockito.eq 进行参数匹配,不能使用类似mockito.any()这种
四、常见问题与解决方式
1.测试用例报错:org.mockito.exceptions.misusing.WrongTypeOfReturnValue:
将when(test.do()).thenReturn(mockData)改为
doReturn(mockData).when(test).do()形式
2.错误:ScriptEngineManager providers.next(): javax.script.ScriptEngineFactory: Provider jdk.nashorn.api.scripting.NashornScriptEngineFactory not a subtype ScriptEngineManager providers.next(): javax.script.ScriptEngineFactory: Provider jdk.nashorn.api.scripting.NashornScriptEngineFactory not a subtype 2017-06-09 13:37:24,660 main ERROR No ScriptEngine found for language javascript. Available languages are: 2017-06-09 13:37:24,776 main WARN No script named {} could be found
原因:主要由于PowerMock与一些组件,比如log4j有冲突,但不影响测试用例的执行,可以直接忽略
解决:在测试类上加上注解
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*","javax.script.*"})
public class MockUtils {
}
3.Junit5 中mock 静态方法
junit5不支持PowerMockito,但可以使用MockedStatic.
注:Mockito3.4.0 版本之前是不支持MockedStatic的
@Test
public void staticTest(){
MockedStatic<Static5Class> static5ClassMockedStatic = Mockito.mockStatic(Static5Class.class);
static5ClassMockedStatic.when(Static5Class::str).thenReturn("bbb");
Assert.assertEquals("bbb",Static5Class.str());
}
五、JUNIT5与Mocktio3
1.How to resolve Unneccessary Stubbing exception
在一段代码中,可能会存在多段代码逻辑分支。
我们将所有的外部依赖方法都mock之后,如果在Test流程中因为某个逻辑分支没有使用到,Mockito会报不必要的存根错误。
如下图所示,我写了6个方法的mock,但是其实在Test流程中,因为只跑了某个逻辑分支,该分支只用到了其中的4个mock。就会报错。
解决方法:
@MockitoSettings(strictness = Strictness.LENIENT)
class MyTest{
......
}
2.Mockito3 中必须手动关闭mockStatic
错误描述:图片:
解决方案:每个测试用例调用完成MockedStatic<RpcContext> rpcContextMockedStatic = Mockito.mockStatic(RpcContext.class);之后必须手动关闭:
rpcContextMockedStatic.close();