Mockito使用问题记录

一、背景

公司在用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没有生效。

参见:如何mock一个List类型的属性

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的方法

  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值