文章目录
一、背景
公司在用Mockito进行单元测试,写单测的时候总会遇到一些小问题,顺手记录下来。
二、遇到问题记录
2.1 mock update 报错
2.1.1 问题现象
当update使用如下写法时,mock会报错
被mock代码示例:
mock语句:
when(platformMallOrderServcie.update(any())).thenReturn(true);
报错:
ERROR com.longfor.gf.lw.ordercenter.business.OrderBusiness - callbackMerchant回调异常,原因:cannot find column’s cache for “com.longfor.gf.lw.ordercenter.entity.PlatformMallOrder”, so you cannot used “class com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper”!
com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: cannot find column’s cache for “com.longfor.gf.lw.ordercenter.entity.PlatformMallOrder”, so you cannot used “class com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper”!
2.1.2 问题原因
根据测试,发现只有update时使用lambda进行set操作会有问题。推测是mock框架和mybatis-plus框架有冲突。
2.1.3 解决方案
解决方法有两个,两种方案均可实现mock不报错。
第一种解决方案:修改update写法
不使用lambda即可。修改后代码如下:
第二种解决方案:修改mock方法
除非mock时发现代码有问题,一般情况下,不会因为mock修改代码,那么就要修改mock方法。报错中提示是目标类没有加载进mybatis-plus的缓存中,那么mock时手动将其加载进缓存即可。代码如下:
根据版本号选择
TableInfoHelper.initTableInfo(null, PlatformMallOrder.class);
或
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), RefundTradeProcessPool.class);
改后如下:
TableInfoHelper.initTableInfo(null, PlatformMallOrder.class);
when(platformMallOrderServcie.update(any())).thenReturn(true);
注:如果有其他方案,欢迎大家补充。
2.2 mock mybatis-plus生成的service报错
2.2.1 问题现象
以PlatformMallGoodsServiceImpl类为例。此类定义如下
public class PlatformMallGoodsServiceImpl extends ServiceImpl<PlatformMallGoodsMapper, PlatformMallGoods> implements IPlatformMallGoodsService
所以应该mock对应的mapper方法
when(platformMallGoodsMapper.update(any(PlatformMallGoods.claaa),any(UpdateWrapper.class))).thenReturn(1);
代码中有如下写法,mock时会报空指针
this.update(goodsUpdateWrapper)
2.2.2 查找问题点
通过debug可以找到对应的报错代码
发现是mapper没有注进去。
2.2.3 问题原因
原因是使用的注解不同
// mock目标类的注解
@InjectMocks
private PlatformMallGoodsServiceImpl platformMallGoodsService;
// 其他被mock的类
@Mock
private ISecondhandGoodsDetailService secondhandGoodsDetailService;
@InjectMocks标记的类会被实例化,@Mock标记的类会被注入到@InjectMocks标记的类中。
2.2.4 解决方案
mock方法中手动注入mapper即可,代码写法如下
@Mock
private PlatformMallGoodsMapper platformMallGoodsMapper;
// 单测方法中增加mapper注入
ReflectionTestUtils.setField({@InjectMocks标记的类}, "baseMapper", {@InjectMocks标记的类对应的mapper});
按照上述写法,此例中添加的mock代码如下
@Mock
private PlatformMallGoodsMapper platformMallGoodsMapper;
// 单测方法中增加mapper注入
ReflectionTestUtils.setField(platformMallGoodsService, "baseMapper", platformMallGoodsMapper);
2.3 mock List失败
2.3.1 问题现象
@Mock一个List变量不生效,代码如下
@Mock
private List<TimeFormatStrategy> mockStrategies;
在真正运行时会报NPE,因为strategies为null,报错代码如下
for (TimeFormatStrategy strategy : strategies)
2.3.2 问题原因
具体原因没有找到,我观察到在单测方法中list会被mock出来,但走到真正的方法内list就变成null了,仿佛mock没有生效。
2.3.3 解决方案
将@Mock换成@Spy注解,然后增加一个赋值逻辑即可
@Spy
private List<TimeFormatStrategy> mockStrategies = new ArrayList<>();
// 不用一定放到Before里,放在对应的Test方法中也可以,我是懒得一个一个放就这么写了
@Before
public void init() {
mockStrategies.add(mockDefaultTimeFormatStrategy);
}
2.4 mock 抽象类
2.4.1 问题现象
@InjectMocks 抽象类报错,代码如下
// 抽象类定义
public abstract class AbstractDiscountJob{
// 中间内容省略
}
// 单测中的写法
@InjectMocks
private AbstractDiscountJob abstractDiscountJobUnderTest;
报错如下
org.mockito.exceptions.base.MockitoException:
Cannot instantiate @InjectMocks field named 'abstractDiscountJobUnderTest'.
You haven't provided the instance at field declaration so I tried to construct the instance.
However, I failed because: the type 'AbstractDiscountJob is an abstract class.
Examples of correct usage of @InjectMocks:
@InjectMocks Service service = new Service();
@InjectMocks Service service;
//also, don't forget about MockitoAnnotations.initMocks();
//and... don't forget about some @Mocks for injection :)
at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl$1.withBefores(JUnit45AndHigherRunnerImpl.java:27)
at org.junit.runners.BlockJUnit4ClassRunner.methodBlock(BlockJUnit4ClassRunner.java:276)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.java:37)
at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.java:62)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
Caused by: org.mockito.exceptions.base.MockitoException: the type 'AbstractDiscountJob is an abstract class.
... 17 more
2.4.2 问题原因
@InjectMocks 可以创建一个能够访问的实例,但抽象类不能被实例化。
报错信息的开头部分已经写明原因
Cannot instantiate @InjectMocks field named ‘abstractDiscountJobUnderTest’.
You haven’t provided the instance at field declaration so I tried to construct the instance.
However, I failed because: the type 'AbstractDiscountJob is an abstract class.
2.4.3 解决方案
报错信息开头部分往后看,作者也提示了解决办法
Examples of correct usage of @InjectMocks:
@InjectMocks Service service = new Service();
@InjectMocks Service service;
//also, don’t forget about MockitoAnnotations.initMocks();
//and… don’t forget about some @Mocks for injection 😃
改成下方写法即可
@InjectMocks
private AbstractDiscountJob abstractDiscountJobUnderTest = mock(AbstractDiscountJob.class, Mockito.CALLS_REAL_METHODS);
2.4.4 采坑记录
虽然报错信息说了解决方法,然而我当时并没有仔细看,直接去冲浪搜解决办法了 =_=|||
网上针对mock抽象类有两个问题,一个是如何 InjectMocks 一个抽象类,就是我遇到的问题,另一个是InjectMocks的实例中引用了抽象类,要mock这个抽象类。
这两个问题容易混淆,有的作者写的不清楚,需要看完全文才知道解决的是哪个问题。
网上的方法的确可以实例化抽象类,也能进到方法里,但无法将单测中依赖的@Mock参数带进去,所以改成了现在的这种写法。
2.5 mock静态类/静态方法
2.5.1 问题现象
mock静态类时报错,静态方法如下
public class ShardingUtil {
public static ShardingVO getShardingVo() {
return (ShardingVO)contextHolder.get();
}
}
单测代码:
import com.xxl.job.core.util.ShardingUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.Map;
@RunWith(MockitoJUnitRunner.class)
public class AbstractOrderJobHandlerTest {
@Test
public void testDoExecute() {
// 创建AbstractOrderJobHandler的Mock对象
AbstractOrderJobHandler handler = Mockito.mock(AbstractOrderJobHandler.class, Mockito.CALLS_REAL_METHODS);
// 模拟ShardingUtil.ShardingVO对象
ShardingUtil.ShardingVO shardingVO = new ShardingUtil.ShardingVO(0, 3);
// 设置模拟的ShardingUtil.ShardingVO对象
Mockito.when(ShardingUtil.getShardingVo()).thenReturn(shardingVO);
// 调用doExecute方法
handler.doExecute(null);
// 验证orderJobHandlerMethod方法是否被调用
Mockito.verify(handler).orderJobHandlerMethod(Mockito.any(Map.class));
}
}
报错信息:
org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
when(mock.getArticles()).thenReturn(articles);
Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
Those methods *cannot* be stubbed/verified.
Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.
出错代码:
Mockito.when(ShardingUtil.getShardingVo()).thenReturn(shardingVO);
2.5.2 问题原因
3.4.0版本之前的 mockito 无法 mock 静态类和静态方法,如果你的是3.4.0及以上的版本,可以参考这位大佬:Mockito 如何 mock 静态方法
我的是1.10.19,鉴于是老项目,不敢升版本,所以我用 PowerMockito 实现。
2.5.3 解决方案
直接上代码
import com.xxl.job.core.util.ShardingUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.Map;
// 换成 PowerMockito
@RunWith(PowerMockRunner.class)
// 增加静态类
@PrepareForTest({ShardingUtil.class})
public class AbstractOrderJobHandlerTest {
@Test
public void testDoExecute() {
AbstractOrderJobHandler handler = Mockito.mock(AbstractOrderJobHandler.class, Mockito.CALLS_REAL_METHODS);
ShardingUtil.ShardingVO shardingVO = new ShardingUtil.ShardingVO(0, 3);
// 增加静态类 mock
PowerMockito.mockStatic(ShardingUtil.class);
// 使用 PowerMockito 模拟 ShardingUtil.ShardingVO对象
PowerMockito.when(ShardingUtil.getShardingVo()).thenReturn(shardingVO);
handler.doExecute(null);
Mockito.verify(handler).orderJobHandlerMethod(Mockito.any(Map.class));
}
}
2.6 mock void方法
2.6.1 问题现象
public class MyClass {
// 要 mock 的方法
public void targetMethod((Map<String, Object> param) {
// 省略方法实现
}
}
2.6.2 解决方案
只是将 void 方法 mock 掉,可以参考如下写法
@Test
public void testNoticePayer() {
MyClass myClass = mock(MyClass.class);
doNothing().when(myClass).targetMethod(anyMap());
}
但 void 方法中有可能修改 param 的值,然后在方法外部获取这个值使用,那么可以用Mockito类提供的doAnswer方法
// 提供void方法的类
public class MyClass {
public void targetMethod(Map<String, Object> param) {
param.put("aa", "111");
}
}
// 业务方法的类
public class BusinessClass {
public void businessMethod(){
MyClass myClass = new MyClass();
Map<String, Object> param = new HashMap<>();
myClass.targetMethod(param);
System.out.println(param);
}
}
// 单测方法
@Test
public void testTargetMethod() {
MyClass myClass = mock(MyClass.class);
// 此处为修改入参的方法
doAnswer((Answer) invocation -> {
// 获取方法的第一个入参
Map<String, Object> arg0 = (Map<String, Object>)invocation.getArguments()[0];
arg0.put("aaa","100");
return null;
}).when(myClass).targetMethod(anyMap());
businessMethod();
}
三、致谢
本文参考了以下博客,在此感谢所有作者以及积极回答问题的同学
参考链接:
Mockito关于抽象类的问题
使用Mockito测试抽象类
Mockito 如何 mock 静态方法
用Mockito来mock返回值类型为void的方法