mockiot+junit5
mokito+junit5使用
前要
Mock测试:在测试过程中,对于某些不容易获取的数据,用一个虚拟的对象来代替测试的方法。解决不同的单元之间由于耦合而难于开发、测试的问题。
Mockito:是一种 Java Mock 框架,主要是用来做 Mock 测试,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等。同时记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。
Mock对象:一个模拟真实对象行为的虚假对象。
打桩/插桩:对Mock对象的行为进行定义。即定义当调用到对象的某方法时返回特定的值(可抛出异常)。不会真正执行方法实际逻辑。
结果断言:对方法执行的结果进行验证
行为验证:验证方法的调用顺序、调用次数等
单元测试流程:创建mock对象,对mock对象行为进行定义,调用测试方法,对响应结果进行验证。
Mockito使用
mock、syp对象区别
方法插桩 | 方法不插桩 | 作用对象 | |
---|---|---|---|
mock对象 | 执行插桩逻辑 | 返回mock对象方法的默认值(0,null……) | 类、接口 |
spy对象 | 执行插桩逻辑 | 调用真实方法 | 类、接口 |
mock对象:会创建一个虚假的测试类对象,包含测试类所有方法,但方法都是空的没有具体实现;
未进行打桩时,调用mock对象方法,不会调用到实际方法,会返回方法默认值(0、null……)
syp对象:为真实对象创建一个监控对象,是特殊的mock对象;未进行打桩时,调用syp对象方法,会调用到实际方法。
mock对象初始化方式
方法一(推荐使用) | 方法二 | 方法三 | |
---|---|---|---|
junit4 | @RunWith(MockitoJUnitRunner.class)+@Mock注解 | Mockito.mock(A.class) | MockitoAnnotations.openMocks(this)+@Mock |
junit5 | @ExtendWith(MockitoExtension.class)+@Mock注解 | Mockito.mock(A.class) | MockitoAnnotations.openMocks(this)+@Mock |
举例:类注解+@Mock注解
@ExtendWith(MockitoExtension.class)
class SubAccBalanceProcessorTest {
@Mock
private SubAccBalanceProcessor subAccBalanceProcessorUnderTest;
}
常用注解
注解 | 使用限制 | 说明 |
---|---|---|
@Test | 方法 | 修饰的方法为测试方法,可单执行 |
@InjectMocks | 标注的属性必须是实现类 |
1.会创建被测试类对应的实例对象(不是mock对象), 2.所以通常配合@Spy注解一起使用,使其变为默认调用真实方法的mock对象。 3.将模拟对象注入到被测试对象中,自动处理依赖关系。(原理:构造器注入、setter注入、字段反射注入) |
@BeforeEach | 方法 | 每个测试方法执行前运行;不可以单独运行该方法 |
@AfterEach | 方法 | 每个测试方法执行后运行;不可以单独运行该方法 |
@Mock | 属性 | 创建一个mock对象 |
@Spy | 属性 | 创建一个syp对象 |
@MockBean | 属性 | 将 mock的 对象通过类型或名字替换Spring环境中已存在的bean,若Spring环境中不存在对应bean则将 mock 的对象作为 bean 添加到上下文中,从而达到对这些bean进行mock |
@SpyBean | 属性 | 同上,通过类型或名字包装Spring环境中已存在的bean,从而达到对这些bean进行mock |
@Test 测试方法的入口;可单独运行
@InjectMocks
//若@InjectMocks声明的变量需要用到mock/spy对象,mockito会自动使用当前类里面的mock或spy进行按类型或名字的注入(原理:构造器注入、setter注入、字段反射注入)
//被@InjectMocks标注的属性必须是实现类,因为mockiot会创建对应的实例对象
//未经过mockito处理的普通对象,因此常配合!@Spy注解使用,使其变为默认调用真实方法的mock对象
@BeforeEach 每个测试方法前运行;不可以单独运行该方法
@AfterEach 每个测试方法后运行;不可以单独运行该方法
@BeforeAll 在类中所有方法前运行;static修饰;不可单独运行该方法
@AfterAll 在类中所有方法后运行;static修饰;不可单独运行该方法
方法插桩
方法插桩有两种写法
写法 | 作用于syp对象时 | 使用场景 | |
---|---|---|---|
方法1 | when(mockList.size()).thenReturn(10); | 插桩时会先调用一次真实方法。后续再调用到该方法时执行插桩逻辑;达不到mock的目的,所以不建议作用于syp对象 | 可用于mock对象;方法必须有返回值 |
方法2(推荐使用) | doReturn(10).when(mockList).size(); | 插桩时不会调用真实方法。后续再调用到该方法时执行插桩逻辑;达到了mock的目的, | 可用于mock、spy对象;可用于无返回值对象方法 |
使用举例:
//1.固定参数匹配
List mockList = mock(List.class);//创建一个List的mock对象
doReturn("1").when(mockList).get(1);//当调用参数为1的List对象get方法时,返回固定值“1”
Object result = mockList.get(1);//调用mockList.get(1)进行测试,返回结果固定为"1"
//2.任意参数匹配(注意:anyInt(),any()都不包括Null,传递NULL不会被匹配;若需要匹配NULL参数,请使用适配器isNull())
doReturn("1").when(mockList).set(anyInt(),any());//当调用参数为任意的List对象set方法时,返回固定值“1”
//3.方法多个入参时,要么都使用参数匹配器,要么都不使用,否则运行会报异常
doReturn("1").when(mockList).set(1,any());//执行会报错
//注意:通常使用参数匹配器,用any()即可,没必要指定到具体类型anyInt()、anyString()等;有一种情况:当此方法是一个重载的方法时参数匹配器必须指定到具体的类型即anyInt()、anyString()等,否则程序无法判断该调用具体那个方法,编译不通过。
//4.void返回值插桩(只有无返回值的方法可使用doNothing())
doNothing().when(mockList).clear();//调用方法mockList.clear()时什么也不做
//5.方法调用抛出异常插桩
doThrow(RuntimeException.class).when(mockList).get(anyInt());//调用方法mockList.get()时返回RuntimeException异常
//6.连续插桩,第一次调用方法mockList.size()时返回1,第二次调用返回2,第三次及以上调用返回3
doReturn(1, 2, 3).when(mockList).size();
//7.连续插桩,第一次调用方法mockList.size()时返回异常,第二次调用返回1,第三次及以上调用返回2
doThrow(RuntimeException.class).doReturn(1, 2).when(mockList).size();
//8.实现指定逻辑插桩 演示代码解释:当调用任意参数方法mockList.get()时,返回list.get(2)的值。
when(mockList.get(anyInt())).thenAnswer(new Answer<String>() {
//泛型表示要插桩方法的返回值类型
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
//表示获取插桩方法(list.get())参数为2的值
Integer argument = invocation.getArgument(2, Integer.class);
return argument.toString();
}
});
//9.执行真正方法
doCallRealMethod().when(mockList).size();//当调用方法mockList.size()时,执行其原始方法
结果断言
使用举例:
List mockList = mock(List.class);//创建一个List的mock对象
doReturn(2).when(mockList).size();//方法插桩:当调用方法mockList.size()时返回固定值2
//1.断言调用mockList.size()方法返回值等于2
assertEquals(mockList.size(),2);//校验成功
//2.断言调用mockList.size()方法返回值不等于2
assertNotEquals(mockList.size(),2);//校验失败
//3.断言调用mockList.size()方法返回值为空
assertNull(mockList.size());//校验失败
//4.断言调用mockList.size()方法返回值不为空
assertNotNull(mockList.size());//校验成功
//5.断言调用mockList.add(0)方法返回值为true
assertTrue(mockList.add(0));
//6.断言调用mockList.add(0)方法返回值为false
assertFalse(mockList.add(0));
//7.断言调用handlerTest.call()方法会抛出异常LunaBusinessException
assertThrows(LunaBusinessException.class,()->{
handlerTest.call();
});
//更多断言方法详见org.junit.jupiter.api.Assertions类
行为验证
使用举例:
//1.校验参数为0的方法get调用过一次(两种写法如下)
verify(mockList).get(0);
verify(mockList,times(1)).get(0);
//2.校验方法mockList.get(0)没有调用过(两种写法如下)
verify(mockList,times(0)).get(0);
verify(mockList,never()).get(0);
//3.校验方法最少或最多调用了多少次
verify(mockList,atLeast(1)).get(0);
verify(mockList,atMost(1)).get(0);
//判断一个对象是否为mock对象
mockingDetails(syncCurrentBankBalanceBatchHandlerUnderTest).isMock();
案例整理
1.一个完整的demo
实际方法
@Service
public class UserUtil {
@Autowired
private UserMapper userMapper;
public Integer getUserAge(String code){
UserPO userPO=userMapper.getByCode(code);
if(StrUtil.equals(userPO.getName(),"尔尔")){
return 100;
}
return userPO.getAge();
}
}
测试方法
@ExtendWith(MockitoExtension.class)
class UserUtilTest {
/**
* @Mock:与类注解@ExtendWith(MockitoExtension.class一起使用
* 作用:创建一个UserMapper类型的mock对象
*/
@Mock
private UserMapper mockUserMapper;
/**
* @InjectMocks:表明这是要测试的类,会将此现类中代@Mock注解创建的mock对象替换掉原UserUtil类中注入的UserMapper
* @Spy:创建UserUtil类型的spy对象,方法未打桩时默认会调用实际方法。
*/
@InjectMocks
@Spy
private UserUtil userUtilUnderTest;
@Test
void testUserAgeMapping() {
//创建一个假的返回对象
UserPO userPO = new UserPO();
userPO.setName("尔尔");
userPO.setAge(10);
userPO.setCode("001");
//方法打桩:当调用方法mockUserMapper.getByCode("001")时,返回固定值userPO
doReturn(userPO).when(mockUserMapper).getByCode("001");
//调用要测试的方法
Integer result = userUtilUnderTest.getUserAge("001");
//断言方法返回值是否等于100
assertEquals(100,result );
}
}
2.mock静态类方法
实现:
/**
* mock一个CacheService类型的对象
*/
@Mock
@MockBean
private CacheService cacheService;
@Test
public void test() {
//mock SpringContextHolder类
mockStatic(SpringContextHolder.class);
//方法打桩:当调用方法SpringContextHolder.getBean(CacheService.class)时返回一个mock对象cacheService
when(SpringContextHolder.getBean(CacheService.class)).thenReturn(cacheService);
//方法打桩:当调用方法cacheService.getStandardAreasIdByCode(any())时,返回固定值”123“
when(cacheService.getStandardAreasIdByCode(any())).thenReturn("123");
CacheService bean = SpringContextHolder.getBean(CacheService.class);
String result = bean.getStandardAreasIdByCode("11");
assertEquals(result,"123");//验证通过
}
常见异常信息:Static mocking is already registered in the current thread
原因:多个测试方法中都进行mock静态类,但未及时关闭;mock静态类只能一个线程mock一次,需要用完后关闭。
解决方法:在@BeforeEach 进行mock,在@AfterEach 里进行关闭。这两个注解注释的方法,在每个测试类执行前后都会调用;在测试方法中正常调用即可,不再mock静态类。
编码举例:
//设置类属性
private MockedStatic<SpringContextHolder> springContextHolderMockedStatic;
//调用测试方法前mock静态类
@BeforeEach
void setUp() {
springContextHolderMockedStatic = mockStatic(SpringContextHolder.class);
}
//调用测试方法后关闭
@AfterEach
void after() {
springContextHolderMockedStatic.close();
}
@Test
public void test() {
//直接使用静态方法
when(SpringContextHolder.getBean(CacheService.class)).thenReturn(cacheService);
when(cacheService.getStandardAreasIdByCode(any())).thenReturn("123");
String result == SpringContextHolder.getBean(CacheService.class).getStandardAreasIdByCode("11");
assertEquals(result,"123");//验证通过
}
3.mock私有方法
//私有方法需要通过反射调用进行测试
//参数1:测试类对象,参数2:私有方法名,参数3+:私有方法入参
final MarginReqHead result3 =ReflectionTestUtils.invokeMethod(marginAmtServiceImplUnderTest,"analysisReq",reqPacket3, marginRespHead);
4.mock测试类方法中new的对象
实际方法:
public String getName(){
SubAccForcedPayProcessor processor = new SubAccForcedPayProcessor();
return processor.getInterfaceName();
}
测试方法:
(我们期待对方法getName()中的new的 SubAccForcedPayProcessor对象进行mock,从而达到对方法processor.getInterfaceName()进行打桩的目的)
@Test
public void testGetName() {
//mock构造函数;将测试类方法new SubAccForcedPayProcessor()的对象替换为一个mock对象。
MockedConstruction<SubAccForcedPayProcessor> mockedConstruction = mockConstruction(SubAccForcedPayProcessor.class, (mock, context) -> {
//当调用方法new SubAccForcedPayProcessor().getInterfaceName()时返回固定值“123”
doReturn("123").when(mock).getInterfaceName();
});
String name = marginCommonProcessorUnderTest.getName();
mockedConstruction.close();
assertEquals(name,"123");//校验通过
}
特别注意:mock 构造函数同mock静态类,只能一个线程mock一次需要用完后需要关闭。可使用try-with-resources 或 案例一中举例的@BeforeEach、@AfterEach 。
5.对mock的私有属性赋值
使用 ReflectionTestUtils.setField()
@Mock
private CacheService cacheService;
@Test
void testCall() throws Exception {
//为测试类“syncCurrentBankBalanceBatchHandlerUnderTest”的私有属性“cacheService”赋值为一个mock对象
ReflectionTestUtils.setField(syncCurrentBankBalanceBatchHandlerUnderTest, "cacheService", cacheService);
}
6.mock的测试类没有无参构造方法
mock的测试类没有无参构造方法时,不可以使用注解@InjectMocks,可以在@BeforeEach方法中用Mockito.syp()创建一个syp对象
需要测试的方法:
public class SyncCurrentBankBalanceBatchHandler {
private String bankID;
private List<Map<String, Object>> accountInfoList;
private boolean isAccorg;
//含参构造方法
public SyncCurrentBankBalanceBatchHandler(String bankID, List<Map<String, Object>> infoList, boolean isAccorg) {
this.bankID = bankID;
this.accountInfoList = infoList;
this.isAccorg = isAccorg;
}
测试类编写:
@ExtendWith(MockitoExtension.class)
class SyncCurrentBankBalanceBatchHandlerTest {
//需要测试的类
private SyncCurrentBankBalanceBatchHandler handlerTest;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
springContextHolderMockedStatic = mockStatic(SpringContextHolder.class);
List<Map<String, Object>> mockInfoList=new ArrayList<>();
HashMap<String, Object> map = new HashMap<>();
map.put("actnum","001");
map.put("actcur","1");
mockInfoList.add(map);
//手动创建syp对象
handlerTest = Mockito.spy(new SyncCurrentBankBalanceBatchHandler("bankID", mockInfoList,
false));
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)