A. 概念
Mock: 在进行单元测试时,往往测试的方法在被执行时会调用其它的方法,而为了保证测试的单元性和独立性,我们通常创建一个模拟方法来模拟这个在被测试方法中被调用的其它方法。而实现这个功能我们通常要用到@Mock及其相关的关键字。
参数化设置:我们可以使用参数化设置来用一个单元测试测试多个不同的参数。
B. Mock的使用
通常测试类的样式是这样的:
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@ExtendWith(MockitoExtension.class) // 开启Mockito功能
class classTest {
@Mock // 需要被模拟的对象
private MockObject mockObject1;
@Mock // 需要被模拟的对象
private MockObject mockObject2;
@InjectMocks // 被测试的类和对象
private testClass testObject;
}
I. @Mock
使用@Mock
可以模拟一个对象,当被测试方法中此对象被使用时,直接进行模拟而不是走正常的方法。
II. @InjectMocks
使用@InjectMocks
可以标注需要做单元测试的类和对象。
III. 方法模拟
import static org.mockito.Mockito.when;
when(mockObject.mockMethod()).thenReturn(someValue)
当我们需要模拟有返回值的方法时可以使用when().thenReturn()
,这样被Mock的方法会直接放回设置好的值。
import static org.mockito.Mockito.doNothing;
doNothing().when(mockObject).someMethod();
当需要模拟一个void方法时且不需要返回值时,我们可以使用doNoting().when()。
import static org.mockito.Mockito.when;
when(mockObject.mockMethod())
.then(importParameter -> {
...
// return someValue;
});
当我们需要模拟一个有复杂逻辑的方法时可以在then
里使用Lambda函数来实现。→
左边是传入值,右边是方法体。
模拟静态方法
public class StaticUtils {
private StaticUtils() {}
public static List<Integer> range(int start, int end) {
return IntStream.range(start, end)
.boxed()
.collect(Collectors.toList());
}
public static String name() {
return "Baeldung";
}
}
@Test
void givenStaticMethodWithNoArgs_whenMocked_thenReturnsMockSuccessfully() {
assertThat(StaticUtils.name()).isEqualTo("Baeldung");
try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
utilities.when(StaticUtils::name).thenReturn("Eugen");
assertThat(StaticUtils.name()).isEqualTo("Eugen");
}
assertThat(StaticUtils.name()).isEqualTo("Baeldung");
}
如果我们想模拟静态方法可以使用try函数,并在代码块中按照如上例子对静态方法进行模拟。如果需要模拟多个静态方法,可以用try函数嵌套来进行模拟。
IV. 验证
在模拟完整个方法后,我们需要对结果进行验证。通常我们使用``org.junit.jupiter.api.Assertions.*;
中的方法就足够了,但是有时候我们也需要用到verify
函数。
import static org.mockito.Mockito.verify;
verify(mockObject, times(num)).someMethod();
verify(mockObject, never(num)).someMethod();
verify(mockObject).someMethod(); // 等价于verify(mockObject, times(1)).someMethod();
verify(mock, atLeast(2)).someMethod("was called at least two times");
verify()
****函数可以计算指定对象的指定方法在整个模拟过程中被执行的次数。
V. 参数模拟
import static org.mockito.ArgumentMatchers.any;
any()
isA(Class)
notNull()
isNull()
anyInt()
anyList()
...
有时候我们Mock方法具体传入的值并不关心,这个时候我们可以使用any
函数来模拟传入的值。
Note: When()里面的方法要么参数全是any(), 要么全不是,不然就会报错。
VI. 常用的模拟场景Template
1. 模拟Controller Template
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class SomeControllerTest {
@Mock
private SomeService someService;
@InjectMocks
private SomeController someController;
@Test
void testSomeMethod() {
MockHttpServletRequest request = new MockHttpServletRequest();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
ResponseEntity<?> responseEntity = someController.someMethod(someValue);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
verify(someService, times(1)).someMethod(someValue);
}
2. ExceptionHandlerTest Template
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import static org.junit.jupiter.api.Assertions.assertEquals;
class APIExceptionHandlerTest {
// Exceptionhandler 类
private ExceptionHandler apiExceptionHandler;
@BeforeEach
void setUp() {
apiExceptionHandler = new APIExceptionHandler();
}
@Test
void handleAccessDeniedException() {
AccessDeniedException exception = new AccessDeniedException("没有权限");
ResponseEntity<String> responseEntity = apiExceptionHandler.handleAccessDeniedException(exception);
assertEquals(HttpStatus.FORBIDDEN, responseEntity.getStatusCode());
assertEquals("没有权限", responseEntity.getBody());
}
}
C. 测试Collection结果
当返回结果是Collection的时候,我们通常可以定义一个方法,遍历这个Collection从而达到检验的效果。我们可以活用集合类的stream()方法来进行测试。
assertStudentName("David", studentList);
assertStudentName("Daniel", studentList);
assertStudentName("Anna", studentList);
public void assertStudentName(String name, List studentList) {
assertTrue(studentList.stream().anyMatch(student -> Objects.equals(student.getName(), name);
}
D. 参数化设置
我们可以使用参数化设置来用一个单元测试测试多个不同的参数。
- 最基本的,我们可以使用
@ValueSource
、@EnumSource
和@CsvSource
来传递多个参数。 - 我们也可以使用
@MethodSource
来构建返回类型为Stream的静态工厂方法来生产参数并进行传递。如果每次想传递多个参数进行测试可以使用Stream<Arguments>在每一个Arguments中储存多个参数并传递。
详见JUNIT 5官方文档:https://doczhcn.gitbook.io/junit5/index/index-2/parameterized-tests