一,简介
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 {@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@Testpublic void test1() {// true 判断某对象是不是mock对象System.out.println(" " + Mockito.mockingDetails(mockUserService).isMock().isMOck());// falseSystem.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;@BeforeEachpublic void init() {mockUserService = Mockito.mock(UserService.class);spyUserService = Mockito.spy(UserService.class);}@Testpublic void test1() {// true 判断某对象是不是mock对象System.out.println(" " + Mockito.mockingDetails(mockUserService).isMock().isMOck());// falseSystem.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 {@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@BeforeEachpublic void init() {MockitoAnnotations.openMocks(this);}@Testpublic void test1() {// true 判断某对象是不是mock对象System.out.println(" " + Mockito.mockingDetails(mockUserService).isMock().isMOck());// falseSystem.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 {@Mockprivate UserService mockUserService;@Testpublic 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*/@Testpublic 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*/@Testpublic void test2() {UserUpdateReq userUpdateReq1 = new UserUpdateReq();userUpdateReq1.setId(1L);userUpdateReq1.setPhone("1L");//指定参数为userUpdateReq1时调用mockUserService.modifyById返回99Mockito.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返回99Mockito.doReturn(toBeReturned: 99).when(mockUserService).modifyById(userUpdateReq2); int result2 = mockUserService.modifyById(userUpdateReq2); }/*** 对于 mock 对象不会调用真实方法,直接返回 mock对象的默认值* 默认值(int), null(UserVO), 空集合(List)*/@Testpublic void test1() {UserVO userVO = mockUserService.selectById(1);// nullSystem.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 {@Mockprivate List<String> mockList;@Mockprivate UserServiceImp1 mockUserServiceImp1;@psyprivate UserServiceImp1 spyUserServiceImp1;/*** 测试verigy*/@Testpublic void test8() {mockList.add("one");// true: 调用mock对象的写操作方法是没效果的Assertions.assertEquals(0, mockList.size());mockList.clear();// 验证调用过1次add方法,且参数必须是oneverify(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();}/*** 执行真正的原始方法*/@Testpublic 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 实现指定逻辑的插桩*/@Testpublic void Test6() {when(mockList.get(anyInt())).thenAnswer(new Answer<String>() {/*** 泛型表示要插桩方法的返回值类型*/@Overridepublic 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);}/*** 多次插桩*/@Testpublic 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());}/*** 抛出异常*/@Testpublic 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);}}/*** 插桩的两种方式*/@Testpublic 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 返回值插桩*/@Testpublic void test2() {// 调用mockList.clear的时候什么也不做doNothing().when(mockList).clear();mockList.clear();// 验证调用了一次clearverfy(mockList,times(wantedNumberOfInvocations:1)).clear();}/*** 指定返回值*/@Testpublic 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@InjectMocksprivate UserService userService;@Mockprivate UserFeatureService userFeatureService;@Mockprivate List<String> mockList;@Testpublic 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 {@Mockprivate List<String> mockList;@Testpublic 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@Resourceprivate UserServiceImp1 userService;@Mockprivate UserFeatureService userFeatureService;@Mockprivate UserMapper userMapper;@Testpublic 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());}@Testpublic 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);}@Testpublic void testSelectById1() {// 配置doReturn(userMapper).when(userService).getBaseMapper();UserVO userVO = userService.selectById(1L);Assertions.assertNull(userVO);}
}
漫谈