UT测试总结

UT测试主要测试单元内部的数据结构、逻辑控制、异常处理等。单元测试实现容易、运行速度快、能完全控制被测试的单元不包含外部依赖、测试用例相互独立无依赖关系。能够帮助发现代码缺陷、修改或者重构代码时确保没有影响现有功能。

对于一些对Bean没有依赖的类的测试(例如一些工具类),仅使用JUnit即可完成单元测试。

对于一些依赖Bean的类进行测试,若其复杂度低,在上层一两个IT测试即可覆盖掉,可以使用IT测试;若其复杂度比较高,可以使用JUnitMockito来完成单元测试,通过使用Mock技术测试可以无视代码依赖关系去测试代码的有效性,mock技术的目的和作用就是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开,对于依赖的Bean进行Mock处理,模拟构造各种Bean的输出来以及待测试方法的输入来覆盖当前方法的所有分支。

Mockito基础

必须使用@RunWith(MockitoJUnitRunner.class)注解,否则Mock的依赖Bean将为空。@Mock将创建一个Mock,@InjectMocks创建一个实例且自动实例化,mockito会自动注入mockspy成员。UserBaseServiceImpl中通过@Autowired注解或者构造方法等方式注入了IUserBaseDao,就可以通过如下方式使用。

1
2
3
4
5
6
7
8
@RunWith(MockitoJUnitRunner.class)
public class UserBaseServiceTest {
@Mock
private IUserBaseDao userBaseDao;

@InjectMocks
private UserBaseServiceImpl userBaseService;
}

@Mock与@Spy的区别

使用@Mock生成的类,所有方法都不是真实的方法,而且返回值都是NULL。通常在设置测试桩时通过如下方式设置,对于多次调用返回不同值,可以通过多次设置thenReturn

1
2
3
4
5
6
LinkedList mockedList = mock(LinkedList.class);
mockedList.add(11);
assertEquals(null, mockedList.get(0));

when(mockedList.get(0)).thenReturn("first").thenReturn("second");
assertEquals("first", mockedList.get(0));

使用@Spy生成的类,所有方法都是真实方法,返回值都是和真实方法一样的。测试桩设置与@Mock方式有所区别:

1
2
3
4
5
6
LinkedList mockedList = spy(LinkedList.class);
mockedList.add(11);
assertEquals(11, mockedList.get(0));

doReturn("foo").when(spy).get(0);
assertEquals("foo", mockedList.get(0));

Redis测试

有时在进行Mock测试时会遇到redisTemplate,通常在应用中会使用redisTemplate.boundValueOps或者redisTemplate.boundHashOps生成一个BoundValueOperations或者BoundHashOperations对象,再来继续调用具体的处理方法。在设置测试桩时,需要进行两次设置。

1
2
when(redisTemplate.boundValueOps(redisKey)).thenReturn(mock(BoundValueOperations.class));
when(redisTemplate.boundValueOps(redisKey).increment(anyLong())).thenReturn(10L);

参数捕捉

有时会出现一大串复杂的逻辑处理后生成一个或几个参数,用于调用其他的依赖Bean,这时可以通过参数捕捉来验证逻辑中各种情况下生产的参数是否满足预期。若简单参数也可以通过verify直接验证。

1
2
3
4
5
6
7
8
9
BoundHashOperations boundHashOperations = mock(BoundHashOperations.class);
when(redisTemplate.boundHashOps(anyString())).thenReturn(boundHashOperations);

ArgumentCaptor<Map> argument = ArgumentCaptor.forClass(Map.class);

verify(boundHashOperations, times(2)).putAll(argument.capture());

Map<String, CaseFlow> map = new HashMap<>();
assertEquals(map, argument.getValue());

方法调用次数验证

当验证的方法中存在循环、或者复杂度比较高等,导致方法在不同条件下可能存在多次调用的情况,最好验证一下方法的调用次数。或者是用于验证某个逻辑没有被执行或方法没有别调用。

1
2
3
4
5
verify(mock, times(1)).someMethod();
// 至少调用2次
verify(mock, atLeast(2)).someMethod();
// 至多调用5次
verify(mock, atMost(5)).someMethod();

异常处理

在进行一些会抛出异常的测试时,可以通过捕获异常在进行后续校验,可以使用@Test(expected = Exception.class),若有多个地方抛出相同异常但异常信息不同时,该测试方法就不适用了,可以通过如下方式进行异常捕获后进行相关的验证。

1
2
3
4
5
6
7
8
9
10
11
doThrow(new RuntimeException()).when(mockedList).clear();
when(redisTemplate.boundValueOps(any())).thenThrow(new RuntimeException());

Exception error = null;
try {
baselineModelHandler.output(segment, modelData);
} catch (Exception e) {
error = e;
}
assertNotNull(error);
assertEquals("", error.getMessage());

验证调用顺序

1
2
3
4
5
6
7
8
9
10
11
12
List firstMock = mock(List.class);
List secondMock = mock(List.class);

firstMock.add("was called first");
secondMock.add("was called second");

//创建多个mock对象的inOrder
InOrder inOrder = inOrder(firstMock, secondMock);

//验证firstMock先于secondMock调用
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");

实现ApplicationContextAware接口的类测试

1
2
3
4
Map<String, Object> map = new HashMap<>();
map.put("outputServiceImpl", new OutputServiceImpl(requestService, updateCache, mock(ICache.class)));
when(applicationContext.getBeansWithAnnotation(InvokeListener.class)).thenReturn(map);
dataInvokeService.setApplicationContext(applicationContext);