欧美企业必备技能-Mockito
Posted 浦江之猿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了欧美企业必备技能-Mockito相关的知识,希望对你有一定的参考价值。
欧美企业做开发除了英语是必备技能之外,使用Mockito写unit test(单位测试)
也是必备技能,毕业经历的四家外企,美企,德企,德企,瑞士企业,没有一家不写UT的,中间在一个私企过渡了一下,呵呵,UT是什么?最近,小伙伴们在写UT时遇到了各种问题,借此机会我对UT做一个总结,希望小伙伴们在写UT时不要绕弯路。示例的源码可以直接通过csdn下载也可以通过git导出:https://github.com/igdnss/mockito.git注:这里主要是介绍Mockito的使用,所以需要有一定的junit test 的基础,文中关于junit test 不懂的地方欢迎评论,私信,第一时间解答。
UnitTest 介绍
在介绍Mockito之前,强调Unit Test的几点内容。
- 支持自动化 (这里需要通过jenkins来支持) 。
- 单元测试之间独立不依赖 。
- 单元测试独立于数据库,文件,或者其进程,一个好的单元测试完全独立于外界资源。
- 时间空间透明性,即不论何时何地执行,结果都应该一样。
- 单元测试需要具有一定的意义,不要存粹的去追求测试覆盖率。
- 单元测试追求简短,且重要性等同于源代码。
其中第3天提到ut要独立于外界资源,这看起来很奇怪,没有外界资源ut怎么能跑起来,这就是本篇要要介绍的重点。没有外界资源,我们需要伪造出资源,也就是业内所说mock资源。目前可以提供mock的框架有很多,例如easymock,powermock以及本文要介绍了mockito。
常用标签
本篇主要介绍Mockito,顺便将junit test中使用在方法上的标签也整理出来,方便后面读代码。
@BeforeClass
在所有类执行之前执行,例如设置上下文环境,共享变量等。
@Before
在当前类执行之前执行,例如初始化对象,变量等。
@After
在当前类执行之后执行。
@AfterClass
在所有类执行之后执行
@Test
需要测试业务逻辑
@Test(timeout=xxx)
在timeout时间范围内如果没有执行完测试逻辑,则报错
@Test(expected=Exception.class)
设置当前的测试逻辑抛出的异常,异常类型为Exception.class
@Ignore
忽略当前的测试逻辑,不执行
Mockito介绍
官网: https://site.mockto.org 不太喜欢这个官网,东西有点乱,但毕竟是官方的权威性,再乱也得硬着头皮去看。mockito是一个测试框架,通过mock(模拟)外界资源让测试代码无需依赖真实环境就能跑起来,可以理解为让测试代码在虚拟环境中跑起来。接下来,我会将工作中常用到一些知识点全部整理出来,希望能帮助大家。依赖于自己写的一个简单三层架构的示例进行介绍,代码的逻辑和环境很简单,没有具体的数据库,没有具体的业务逻辑,主要是为了介绍Mockito是如何将各个部分Mock出来并完成UT的。主要测试Controller层与Service层中的方法,有个别Mock的知识点可能没有涉及到Demo中的方法,旨在介绍知识点。其中UnitTest和Mockito的版本分别为:4.13.2,1.10.19
注意:mockito mock的资源不能是局部的,例如局总变量,私有方法是无法mock的。
代码介绍
Controller 层:
通过此层去调用业务层,实现根据id查找用户的接口
/**
*随便写的一个Controller,方便后面介绍UT而存在的
*/
public class FindingController {
UserService userService;
//这里为了方便直接使用构造方法,真实项目中基本都是使用依赖注入的方式
public FindingController(UserService userService) {
this.userService = userService;
}
public String getUser(int id) {
User user = null;
try {
user = userService.findById(id);
} catch (UnsupportedOperationException e) {
return "Error";
}
if(user == null) {
return "No one";
}else {
return "Hello";
}
}
public void clear() {
userService.clear();
}
public List<User> getGroup() {
return userService.getGroup();
}
public int getAge() {
return userService.getAge();
}
}
Mapper层:
/**
* 并没有真正意义上去访问数据库
*/
public class UserMapper {
public User findById(int userId) {
throw new UnsupportedOperationException();
}
}
domain层:
public class Base {
}
public class Client extends Base {
}
public class User extends Base{
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
**service层: **
public class UserService{
UserMapper userMapper;
public User findById(int userId) {
User user = userMapper.findById(userId);
return user;
}
public void clear() {
throw new RuntimeException();
}
public List<User> getGroup() {
throw new RuntimeException();
}
public int getAge() {
return 18;
}
public int getAgeByIdAndName(int id,String name) {
return 18;
}
public String getName(Base base) {
throw new RuntimeException();
}
}
以上代码真的是一位含苞待放的姑娘,一切的一切都是那么的干净清爽,在这些的代码基础之上我们对mockito进行抽丝剥茧。
mock对象
三种最常用的方法。
- mock()
使用此方法必须要在@RunWith(MockitoJUnitRunner.class)中传入MockitoJUnitRunner.class 。这种法一般用于测试方法中局部对象的mock
示例
@Before
public void setUp() {
findingController = mock(FindingController.class);//这里使用的是mock方法
}
- @Mock
可以使用@Mock标签,一般mock全局对象。
示例
@Mock
private UserMapper userMapper;
注:mock的对象只是一个空对象,其内部的属性并没有mock出来,所以直接使用mock的对象调用方法是会报空指针异常,因此使用mock的对象调用方法时,还需要mock方法的调用。或者在@Before中加上MockitoAnnotaion.initMocks(this);
深度mock对象
FindingController getUser()方法中调用了UserService的getUser()返回结果为User,如果这需要测试user.getName(),就得再需要mock一个User 对象。可以使用深度mock来解决这个问题。
@Mock(answer=Answers.RETURNS_DEEP_STUBS)//深度mock
private UserService userService;
mock方法调用
有返回值方法调用
语法一: when(T methodCall).thenReturn(T returnValue);
methodCall:方法调用;returnValue:调用此方法mock出的返回值。注:如果对象是mock出来的,必须要mock它的方法调用,否则会报空指针异常,因为mock的时候只mock一个空对象,其内部的属性并没有mock出来,所以报空指针异常。
语法二: doReturn(Object toBeReturned).when(T mock).方法 。
示例:
- 成功获取id为1的用户。
/**
*成功获取id为1的用户
*/
@Test
public void testGetUserSuccess() {
when(findingController.getUser(1)).thenReturn(successResult);
String result = findingController.getUser(1);
assertEquals(successResult,result);
}
- 使用doReturn成功获取id为1的用户
/**
*使用doReturn成功获取id为1的用户
*/
@Test
public void testGetUserSuccessWith1DoReturn() {
doReturn(successResult).when(findingController).getUser(1);
String result = findingController.getUser(1);
assertEquals(successResult,result);
}
- 成功获取任意id的用户
/**
*成功获取任意id的用户
*/
@Test
public void testGetUserSuccessWithAnyInt() {
when(findingController.getUser(anyInt())).thenReturn(successResult);
String result = findingController.getUser(1);
assertEquals(successResult,result);
}
- 无深度mock对象调用方法
/**
* userService和user对象都是mock出来的,这里可以通过deepMock省略一个对象
*/
@Test
public void testGetUserSuccessWithoutDeep() {
when(userService.findById(1)).thenReturn(user);
User user = userService.findById(1);
when(user.getName()).thenReturn(successResult);
String result = user.getName();
assertEquals(successResult,result);
}
- 深度mock对象调用方法
一步到位,无需多次mock
/**
* deepMock
*/
@Test
public void testGetUserSuccessWithDeep() {
when(userService.findById(1).getName()).thenReturn(successResult);
String userName = userService.findById(1).getName();
assertEquals(successResult,userName);
}
抛出异常方法调用
语法一: when(T methodCall).thenThrow(Class<? extends Throwable>… throwableClasses);这种方法已经被derecated了,基本不用。
语法二: doThrow(Class<? extends Throwable> toBeThrown).when(T mock).方法;
示例:
/**
*方法一:抛出异常
*/
@SuppressWarnings("unchecked")
@Test
public void testGetUserException1WithAnyInt() {
//这种方法已经被deprecated了,可以用方法二
when(findingController.getUser(anyInt())).thenThrow(UnsupportedOperationException.class);
assertThrows(UnsupportedOperationException.class, ()->{ findingController.getUser(1);});
}
/**
*方法二:抛出异常
*/
@Test
public void testGetUserExceptionWithAnyInt() {
doThrow(UnsupportedOperationException.class).when(findingController).getUser(anyInt());
assertThrows(UnsupportedOperationException.class, ()->{ findingController.getUser(1);});
}
无返回值方法调用
语法: doNothing().when(T mock).方法; 调用完方法后需要验证此方法是被调用的次数。verify(T mock, VerificationMode mode).方法。其中mode表示方法被调用的次数,可以不传,默认为times(1)。
示例:
/**
* 测试返回值为空的方法
*/
@Test
public void testClear() {
doNothing().when(findingController).clear();
findingController.clear();
//验证次方法是否被执行过一次
verify(findingController,times(1)).clear();
}
迭代方法调用
语法一: when(T methodCall).thenReturn(Object value,Object … values);
语法二: when(T methodCall).thenReturn(Object value,Object … values);
针对同一个方法,希望每一次的调用结果不同,就应该用到此方法
示例:
/**
* 测试迭代调用的方法,语法一
*/
@Test
public void testGetGroup1() {
when(findingController.getGroup()).thenReturn(list);
when(list.size()).thenReturn(1,2,3,4);
assertEquals(1, list.size());
assertEquals(2, list.size());
assertEquals(3, list.size());
assertEquals(4, list.size());
}
/**
* 测试迭代调用的方法,语法二
*/
@Test
public void testGetGroup2() {
when(findingController.getGroup()).thenReturn(list);
when(list.size()).thenReturn(1).thenReturn(2).thenReturn(3).thenReturn(4);
assertEquals(1, list.size());
assertEquals(2, list.size());
assertEquals(3, list.size());
assertEquals(4, list.size());
}
thenAnswer处理业务逻辑
语法: when(T methodCall).thenAnswer(Answer<?> answer)
当有一些结果为动态结果时,就要用这种方式了。
示例:
/**
* 使用doAnswer测试一定的逻辑. 返回下标值的5倍
*/
@Test
public void testDoAnswer() {
when(findingController.getGroup()).thenReturn(list);
when(list.get(anyInt())).thenAnswer(answer->{
Integer index = answer.getArgumentAt(0, Integer.class);
return String.valueOf(index*5);
});
assertEquals("0", list.get(0));
assertEquals("50", list.get(10));
}
mock对象调用真实方法
语法: when(T methodCall).thenCallRealMethod();
使用mock对象调用真实的方法就需要使用到此种方式。
示例:
/**
* 测试调用真正的方法
*/
@Test
public void testGetAge() {
when(userService.getAge()).thenCallRealMethod();
assertEquals(18, userService.getAge());
}
spy 部分mock
语法: Object o = spy(真实对象);
在有些测试场景下,需要测试真实对象的值,对部分只能使用mock来处理,可以借助spy来完成。
示例:
/**
* 测试部分mock
*/
public void testSpy() {
List<String> userNameList = new ArrayList<String>();
List<String> spyList = spy(userNameList);
spyList.add("A");
spyList.add("B");
spyList.add("C");
assertEquals("A", spyList.get(0));
assertEquals("B", spyList.get(1));
assertEquals("C", spyList.get(2));
assertEquals(false, spyList.isEmpty());
when(spyList.isEmpty()).thenReturn(true);
when(spyList.size()).thenReturn(100);
assertEquals(true, spyList.isEmpty());
assertEquals(100, spyList.size());
}
mock参数匹配
在前面的示例中提到参数匹配,就是当调用一个方法时,应该返回什么样的值。这里主要介绍一下以下几种方式。
isA()类型判断
示例:
测试getName()中传入的是否为Base类型的对象
/**
* 类型判断测试isA
*/
@Test
public void testGetNameWithIsA() {
when(userService.getName(isA(Base.class))).thenReturn(successResult);
String name = userService.getName(new User());
assertEquals(successResult, name);
}
any类型判断
这个方法我基本没有用过,感觉意义不大
示例:
/**
* 类型判断测试any
*/
@Test
public void testGetNameWithAny() {
when(userService.getName(any(User.class))).thenReturn(successResult);
String name = userService.getName(new Client());
assertEquals(successResult, name);
}
通配符的使用
前文中使用过通配符anyInt(), Mockito中提供了好几个通配符,这里举两个例子。使用通配符的时候一定要注意,参数列表中一处使用通配符,就得处处使用通配符,如果想传入特定的值一定要使用eq()方法。通配符的优先级大于eq(),如果同样的方法多个mock,通配符写在最后一句的话会覆盖前面mock的结果。
示例:
/**
* 通配符测试
*/
@Test
public void testGetNameByIdAndName() {
when(userService.getAgeByIdAndName(anyInt(), anyString())).thenReturn(100);
int result1 = userService.getAgeByIdAndName(1, "A");
assertEquals(100, result1);
// 如果想传入指定的值,下面这种方法不支持. Mockito 中如果多个参数,一处使用通配符处处使用通配符。
/*
* when(userService.getAgeByIdAndName(anyInt(), "B")).thenReturn(200); int
* result2 = userService.getAgeByIdAndName(1, "B"); assertEquals(200, result2);
*/
// 传入指定的值必须使用eq()
when(userService.getAgeByIdAndName(anyInt(), eq("B"))).thenReturn(200);
int result2 = userService.getAgeByIdAndName(1, "B");
assertEquals(200, result2);
}
hamcrest 的使用
hamcrest中提供了很多强大的功能,配合assertThat()使用会增加代码的可读性,代码编写风格符合陈述式风格。另外,可以不用使用assertTure(), assertEquals()等断言方法,使用一个assertThat()就可以了。以下示例独立于文章开头的业务代码。但assertThat已经deprecated了,不知道为什么。
示例:
@Test
以上是关于欧美企业必备技能-Mockito的主要内容,如果未能解决你的问题,请参考以下文章