mockito+junit搞定单元测试(2h)
一,简介
1.1 单元测试的特点
- 配合断言使用(杜绝 System.out )
- 可重复执行
- 不依赖环境
- 不会对数据产生影响
- spring 的上下文环境不是必须的
- 一般都需要配合 mock 类框架来实现
1.2 mock 类框架使用场景
要进行测试的方法存在外部依赖(如 db, redis, 第三方接口调用等), 为了能够专注于对该方法(单元)的逻辑进行测试,就希望能虚拟出外部依赖,避免外部依赖成为测试的阻塞项,一般都是测试 service 层即可。
1.3 常用 mock类框架
mock 类框架;用于 mock 外部依赖
1.3.1 mockito
名称:ito: input to output
官网: https://site.mockito.org
官网文档: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
限制:老版本对于 fianl class, final method, static method, private method 均不能被 mockito mock, 目前已支持final class, final method, static method 的 mock, 具体可以参考官网
原理:bytebuddy, 教程: https://www.bilibili.com/video/BV1G24y1a7bd
1.3.2 easymock
1.3.3 powermock
官网:https://github.com/powermock/powermock
与mockito的版本支持关系:https://gitee.com/mirrors/powermock/wikis/Mockito#supported-versions 对 mockito 或 easymock 的增强
1.3.4 JMockit
二,mockito 的单独使用
2.1 mock 对象与 spy 对象
方法插桩 | 方法不插桩 | 作用对象 | 最佳实践 | |
---|---|---|---|---|
mock 对象 | 执行插桩逻辑 | 返回mock对象的默认值 | 类,接口 | 被测试类或其他依赖 |
spy 对象 | 执行插桩逻辑 | 调用真实方法 | 类,接口 | 被测试类 |
2.2 初始化 mock/spy 对象的方式
方法一 | 方法二 | 方法三 | |
---|---|---|---|
junit4 | @RunWith(MockitoJUnitRunner.class) + @Mock等注解 | Mockito.mock(X.class)等静态方法 | MockitoAnnotations.openMocks(this)+@Mock等注解 |
junit5 | @ExtendWith(MockitoExtension.class) + @Mock等注解 |
示例
/**
* 初始化 mock/spy 对象的方式有三种,第一种
* @author zhangdaowen
*/
@ExtendWith(MockitoExtension.class)
public class InitMockOrSpyMethod1 {
@Mock
private UserService mockUserService;
@Spy
private UserService spyUserService;
@Test
public void test1() {
// true 判断某对象是不是mock对象
System.out.println(" " + Mockito.mockingDetails(mockUserService).isMock().isMOck());
// false
System.out.println(""+Mockito.mockingDetails(mockUserService).isMock().isSpy());
System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());
System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());
}
}
/**
* 初始化 mock/spy 对象的方式有三种,第二种
* @author zhangdaowen
*/
public class InitMockOrSpyMethod2 {
private UserService mockUserService;
private UserService spyUserService;
@BeforeEach
public void init() {
mockUserService = Mockito.mock(UserService.class);
spyUserService = Mockito.spy(UserService.class);
}
@Test
public void test1() {
// true 判断某对象是不是mock对象
System.out.println(" " + Mockito.mockingDetails(mockUserService).isMock().isMOck());
// false
System.out.println(""+Mockito.mockingDetails(mockUserService).isMock().isSpy());
System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());
System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());
}
}
/**
* 初始化 mock/spy 对象的方式有三种,第三种
* @author zhangdaowen
*/
public class InitMockOrSpyMethod3 {
@Mock
private UserService mockUserService;
@Spy
private UserService spyUserService;
@BeforeEach
public void init() {
MockitoAnnotations.openMocks(this);
}
@Test
public void test1() {
// true 判断某对象是不是mock对象
System.out.println(" " + Mockito.mockingDetails(mockUserService).isMock().isMOck());
// false
System.out.println(""+Mockito.mockingDetails(mockUserService).isMock().isSpy());
System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());
System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());
}
}
2.3 参数匹配
/**
* 参数匹配; 通过方法签名(参数)来指定哪些方法调用需要被处理(插桩,verify验证)
* @author zhangdaowen
*/
@ExtendWith(MockitoExtension.class)
public class ParamMatcherTest {
@Mock
private UserService mockUserService;
@Test
public void test4(){
List<String> features = new ArrayList<>();
mockUserService.add("乐之者Java", phone:"123", features);
// 校验参数为 "乐之者Java", "123", features 的 add 方法调用了1次
Mockito.verify(mockUserService,MOckito.times(wantedNumberOfInvocations:1)).add("乐之者Java", phone:"123", features);
// 报错:When using matchers, aLL arguments have to be provided by matches
//Mockito.verify(mockUserService,Mockito.times(wantedNumberOfInvocations:1)).add(ArgumentMatchers.anyString(), "123", features);
Mockito.verify(mockUserService,Mockito.times(wantedNumberOfInvocations:1)).add(ArgumentMatchers.anyString(), anyString, anyList());
}
/**
* ArgumentMatchers.any 拦截 UserUpdateReq 类型的任意对象
* 除了any, 还有anyXX(anyLong, anyString) 注意:它们都不包括null
*/
@Test
public void test3() {
Mockito.doReturn(toBeReturned:99).when(mockUserService).modifyById(ArgumentMatchers.any(UserUpdateReq.class));
UserUpdateReq userUpdateReq1 = new UserUpdateReq();
userUpdateReq1.setId(1L);
userUpdateReq1.setPhone("1L");
Mockito.doReturn(toBeReturned: 99).when(mockUserService).modifyById(userUpdateReq1);
int result1 = mockUserService.modifyById(userUpdateReq1);
UserUpdateReq userUpdateReq2 = new UserUpdateReq();
userUpdateReq2.setId(2L);
userUpdateReq2.setPhone("2L");
Mockito.doReturn(toBeReturned: 99).when(mockUserService).modifyById(userUpdateReq2);
int result2 = mockUserService.modifyById(userUpdateReq2);
}
/**
* 测试插桩时的参数匹配, 只拦截userUpdateReq1
*/
@Test
public void test2() {
UserUpdateReq userUpdateReq1 = new UserUpdateReq();
userUpdateReq1.setId(1L);
userUpdateReq1.setPhone("1L");
//指定参数为userUpdateReq1时调用mockUserService.modifyById返回99
Mockito.doReturn(toBeReturned: 99).when(mockUserService).modifyById(userUpdateReq1); // 此处并不产生结果
int result1 = mockUserService.modifyById(userUpdateReq1); // 此处产生结果
UserUpdateReq userUpdateReq2 = new UserUpdateReq();
userUpdateReq2.setId(2L);
userUpdateReq2.setPhone("2L");
//指定参数为userUpdateReq1时调用mockUserService.modifyById返回99
Mockito.doReturn(toBeReturned: 99).when(mockUserService).modifyById(userUpdateReq2);
int result2 = mockUserService.modifyById(userUpdateReq2);
}
/**
* 对于 mock 对象不会调用真实方法,直接返回 mock对象的默认值
* 默认值(int), null(UserVO), 空集合(List)
*/
@Test
public void test1() {
UserVO userVO = mockUserService.selectById(1);
// null
System.out.println("userVO = " + userVO);
UserUpdateReq userUpdateReq1 = new UserUpdateReq();
int i = mockUserService.modifyById(UserUpdateReq1);
System.out.println("i=" + i);
}
}
2.4 方法插桩
指定调用某个方法时的行为(stubbing),达到相互隔离的目的
-
返回指定值
-
void返回值方法插桩
-
插桩的两种方式
- when(obj.someMethod()).thenXxx():其中 obj 可以是 mock 对象
- doXxx(.when(obj).someMethod(): 其中 obj 可以是 mock/spy 对象 或者是无返回值的方法进行插桩
-
抛异常
-
多次插桩
-
thenAnswer
-
执行真正的原始方法
-
verify的使用
/**
* @author zhaodaowen
* @see <a href="http://www.roadjava.com">乐之者java</a>
*/
@ExtendWith(MockitoExtension.class)
public class StubTest {
@Mock
private List<String> mockList;
@Mock
private UserServiceImp1 mockUserServiceImp1;
@psy
private UserServiceImp1 spyUserServiceImp1;
/**
* 测试verigy
*/
@Test
public void test8() {
mockList.add("one");
// true: 调用mock对象的写操作方法是没效果的
Assertions.assertEquals(0, mockList.size());
mockList.clear();
// 验证调用过1次add方法,且参数必须是one
verify(mockList)
// 指定要验证的方法和参数,这里不是调用,也不会产生作用效果
.add("oen");
// 等价于上面的verigy(mockedList)
verify(mockList, times(1)).clear();
// 检验没有调用的两种方式
verify(mockList, times(0)).clear();
verify(mockList, never()).get(1);
// 检验最少或最多调用了多少次
verify(List, atLeast(1)).clear();
verify(List, atMost(3)).clear();
}
/**
* 执行真正的原始方法
*/
@Test
public void test7() {
when(mockUserServiceImpl.getNUmber()).thenCallRealMethod();
int number = mockUserServiceImp1.getNumber();
Assertions.assertEquals(0, number);
// spy对象默认就会调用真实方法,如果不想让它调用,需要单独为它进行插桩
int spyResult = spyUserServiceImpl.getNumber();
Assertions.assertEquals(0, spyResult);
// 不让spy对象调用真实方法
doReturn(1000).when(spyUserServiceImpl).getNumber();
spyResult = spyUserServiceImpl.getNumber();
Assertions.assertEquals(1000, spyResult);
}
/**
* thenAnswer 实现指定逻辑的插桩
*/
@Test
public void Test6() {
when(mockList.get(anyInt())).thenAnswer(new Answer<String>() {
/**
* 泛型表示要插桩方法的返回值类型
*/
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
// getArgument 表示获取插桩方法(此处就是List.get)的第几个参数值
Integer argument = invocation.getArgument(0, Integer.class);
return String.vaLueOf(argument * 100);
}
});
String result = mockList.get(3);
Assertions.assertEquals("300", result);
}
/**
* 多次插桩
*/
@Test
public void test5() {
// 第一次调用返回1, 第二次调用返回2, 第3次及之后的调用都返回3
// when(mockList.size()).thenReturn(1).thenReturn(2).thenReturn(3);
// 可间接写为
when(mockList.size()).thenReturn(1, 2, 3);
Assertions.assertEquals(1, mockList.size());
Assertions.assertEquals(2, mockList.size());
Assertions.assertEquals(3, mockList.size());
}
/**
* 抛出异常
*/
@Test
public void test4() {
// 方法一
doThrow(RuntimeException.class).when(mockList).clear();
try {
mockList.clear();
// 走到下面这一行,说明插桩失败了
Assertions.fail();
} catch (Exception e) {
// 断言表达式为真
Assertions.assertTrue(e instanceof RuntimeException);
}
// 方法二
when(mockList.get(anyInt)).thenThrow(RuntimeException.class);
try {
mockList.get(4);
Assertions.fail();
} catch (Exception e) {
Assertions.assertTrue(e instanceof RuntimeException);
}
}
/**
* 插桩的两种方式
*/
@Test
public void test3() {
when(mockUserServiceImp1.getNumber()).thenReturn(99);
// mockUserServiceImpl.getNumber() = 99 不调用真实方法
System.out.println("" + mockUserServiceImp1.getNumber());
when(spyUserServiceImp1.getNumber()).thenReturn(99);
// getNumber
// spyUserServiceImpl.getNumber() = 99
// spy对象在没有插桩时是调用真实方法的,写在when中会导致先执行一次原方法,达不到mock的目的
// 需使用doXxx.when(obj).someMethod();其中obj可以是mock/spy对象
System.out.println("" + spyUserServiceImp1.getNumber());
doReturn(1000).when(spyUserServiceImpl).getNumber();
}
/**
* void 返回值插桩
*/
@Test
public void test2() {
// 调用mockList.clear的时候什么也不做
doNothing().when(mockList).clear();
mockList.clear();
// 验证调用了一次clear
verfy(mockList,times(wantedNumberOfInvocations:1)).clear();
}
/**
* 指定返回值
*/
@Test
public void Test1 {
// 方法一
doReturn("zero").when(mockList).get(0);
// 如果返回值不相等则本单元测试会失败
Assertions.assertEquals("zero", mockList.get(0));
//方法二
when(mockList.get(1)).thenReturn("one");
Assertions.assertEquals("one", mockList.get(1));
}
}
2.5 @InjectMocks注解的使用
- 作用:若@InjectMocks 声明的变量需要用到 mock/spy 对象,mockito 会自动使用当前类里的 mock 或 spy 成员进行按类型或名字的注入
- 原理:构造器注入、setter注入、字段反射注入
@ExtendWith(MockitoExtension.class)
public class InjectMocksTest {
/**
* 1.被@InjectMocks标注的属性必须是实现类,因为mockito会创建对应的示例对象,默认创建的对象就是未经过mockito处理的普通对象,因此常配合@Spy注解使其变为默认调用真实方法的mock对象
* 2.mockito会使用spy或mock对象注入到@InjectMocks对应的示例对象中
*/
@Spy
@InjectMocks
private UserService userService;
@Mock
private UserFeatureService userFeatureService;
@Mock
private List<String> mockList;
@Test
public void test1() {
int number = userService.getNumber();
Assertions.assertEquals(0, number);
}
}
2.6 断言工具
namcrest:junit4 中引入的第三方断言库,junit5 中被移出,从 1.3 版本后,坐标由 org.hamcrest:hamcrest 变为org.hamcrest:hamcrest
assert: 常用的断言库
junit4 原生断言
junit5 原生断言
@ExtendWith(MockitoExtension.class)
public class AssertTest {
@Mock
private List<String> mockList;
@Test
public void test1() {
when(mockList.size()).thenReturn(999);
// 测试hamcrest的断言
MatchAssert.assertThat(mockList.size(), IsEqual.equalTo(999));
// 测试 assertJ assertThat: 参数为实际的值
Assertions.assertThat(mockList.size().isEquaTo(999));
// junit5原生断言
orj.junit.jupiter.api.Assertions.assertEquals(999, mockList.size());
// junit4原生断言
org.junit.Assert.assertEquals(999, mockList.size());
}
}
三,实战讲解
四,mockito在springboot环境使用(不推荐-)
生成的对象受 spring 管理,相当于自动替换对应类型 bean 的注入
@MockBean
- 类似@Mock
- 用于通过类型或名字 替换 spring 容器中已经存在的bean,从而达到对这些bean进行mock的目的
@SpyBean
- 作用类似@Spy
- 用于通过类型或名字包装spring容器中已经存在的bean,当需要mock被测试类的某些方法时可以使用
/**
* Mock配合spring使用
**/
@SpringBootTest(class = MockitoApp.class)
public class UserServiceImplInSpringTest {
/**
* 不能配置@Spy: Argument passed to when() is not mock!
*/
@SpyBean
@Resource
private UserServiceImp1 userService;
@Mock
private UserFeatureService userFeatureService;
@Mock
private UserMapper userMapper;
@Test
public void testSelectById3() {
//配置方法getById的返回值
UserDo ret = new UserDo();
ret.setId(1L);
ret.setUsername("乐之者java");
ret.setPhone("http://www.roadjava.com");
doReturn(ret).when(userService).getById(1L);
//配置userFeatureService.seLectByUserId的返回值
List<UserFeatureDO> userFeatureDoList = new ArrayList<>();
UserFeatureDO userFeatureDO = new UserFeatureDo();
userFeatureD0.setId(88L);
userFeatureD0.setUserId(1L);
userFeatureDO.setFeatureValue("aaaa");
userFeatureDoList,add(userFeatureDo);
doReturn(userFeatureDoList).when(userFeatureService).selectByUserId(lL);
// 执行测试
UserVo userVO = userService,selectById(userld:1L);
// 断言
Assertions.assertEquals(expected:1,userVO.getFeatureValue().size());
}
@Test
public void testSelectById2() {
// 配置方法getById的返回值
UserDo ret = new UserDo();
ret.setId(1L);
ret.setUsername("乐之者java");
ret.setPhone("http://www.rodajava.com"); // up主的广告
doReturn(ret).when(userService).getById(1L);
UserVo userVO = userService.selectById(1L);
Assertions.assertNotNUll(userVO);
}
@Test
public void testSelectById1() {
// 配置
doReturn(userMapper).when(userService).getBaseMapper();
UserVO userVO = userService.selectById(1L);
Assertions.assertNull(userVO);
}
}
漫谈