欧美企业必备技能-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的几点内容。

  1. 支持自动化 (这里需要通过jenkins来支持) 。
  2. 单元测试之间独立不依赖 。
  3. 单元测试独立于数据库,文件,或者其进程,一个好的单元测试完全独立于外界资源。
  4. 时间空间透明性,即不论何时何地执行,结果都应该一样。
  5. 单元测试需要具有一定的意义,不要存粹的去追求测试覆盖率。
  6. 单元测试追求简短,且重要性等同于源代码。
    其中第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对象

三种最常用的方法。

  1. mock()
    使用此方法必须要在@RunWith(MockitoJUnitRunner.class)中传入MockitoJUnitRunner.class 。这种法一般用于测试方法中局部对象的mock
    示例
	@Before
	public void setUp() {
		findingController = mock(FindingController.class);//这里使用的是mock方法
	}
  1. @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).方法 。
示例:

  1. 成功获取id为1的用户。
	/**
	 *成功获取id为1的用户 
	 */
	@Test
	public void testGetUserSuccess() {
		when(findingController.getUser(1)).thenReturn(successResult);
		String result = findingController.getUser(1);
		assertEquals(successResult,result);
	}
  1. 使用doReturn成功获取id为1的用户
	/**
	 *使用doReturn成功获取id为1的用户 
	 */
	@Test
	public void testGetUserSuccessWith1DoReturn() {
		doReturn(successResult).when(findingController).getUser(1);
		String result = findingController.getUser(1);
		assertEquals(successResult,result);
	}
  1. 成功获取任意id的用户
	/**
	 *成功获取任意id的用户 
	 */
	@Test
	public void testGetUserSuccessWithAnyInt() {
		when(findingController.getUser(anyInt())).thenReturn(successResult);
		String result = findingController.getUser(1);
		assertEquals(successResult,result);
	}

  1. 无深度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);
	}
  1. 深度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的主要内容,如果未能解决你的问题,请参考以下文章

自建APP ,互联网创业的必备技能!

2018开发者技能调查报告之程序员求职必备技能分析

Java-高性能服务器架构设计企业必备技能之Redis集群详解

SQL Server管理员必备技能之性能优化

SQL Server管理员必备技能之性能优化

数据科学家必备的七项关键技能