Mockito不常用的方法
在编写单元测试时,经常会使用Mockito作为mock框架。相信大家对于mock、spy、when、verify的使用已经非常熟悉。但也存在一些场景,这些常见的方法无法满足mock的场景或者verify的条件。此时,就会用到一些别的方法。
方法模拟
doAnswer
用于复杂函数的模拟
例如:方法的参数为一个lambda,此时lambda中的逻辑需要被覆盖,因此使用doAnswer执行lambda中的代码块。
方法:
java
class Service {
@AutoWired
RedisTemplate<String, String> redisTemplate;
void write() {
redisTemplate.pipeline(connection -> {
IntStream.range(0, 3).boxed().forEach(k
-> connection.hIncrBy("key" + k, "hit", 1L));
return null;
});
}
}
测试方法:
java
class ServiceTest {
@InjectMocks
Service service;
@Mock
RedisTemplate<String, String> redisTemplate;
@Test
void testWrite() {
try (RedisConnection redisConnection = mock(RedisConnection.class)) {
doAnswer(invocationOnMock -> {
RedisCallback<Object> sessionCallback = invocationOnMock.getArgument(0, RedisCallback.class);
sessionCallback.doInRedis(redisConnection);
return null;
}).when(redisTemplate).pipeline(any());
service.writeToRedis();
// verify
verify(redisConnection, atLeastOnce()).hIncrBy(any(), any(),
longThat(new GreaterOrEqual<>(1L)));
}
}
}
例如:提交线程池的方法模拟,提交给真实线程池,可以模拟线程池的行为。
java
class TaskRunnerTest {
@Mock
Executor executor;
@InjectMocks
TaskRunner taskRunner;
Executor other = Executors.newFixedThreadPool(2);
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
// 在other线程池执行
doAnswer(invocationOnMock -> {
Runnable runnable = invocationOnMock.getArgument(0, Runnable.class);
executor.execute(runnable);
return null;
}).when(executor).execute(any());
// 不模拟线程池行为的话,可以直接 runnable.run()
}
@Test
void test() {
// 直接执行
taskRunner.run();
taskRunner.run();
// 排队
taskRunner.run();
}
}
例如:需要模拟延迟的情况,以触发超时或计时的逻辑。
java
class ApiRequestTest {
@InjectMock
ApiRequest apiRequest;
@Mock
HttpHelper httpHelper;
@Test
void test() {
// 模拟超时,此时响应会失败
doAnswer(new AnswersWithDelay(100, invocationOnMock -> ""))
.when(restHttpHelper)
.sendGet("http://api1.com", null, String.class);
ApiResp<String> res = apiRequst.ask("sample", null);
assertFalse(res.isSuccess());
// 如果配置了meter,也可以查看meter的耗时
}
}
静态方法模拟
代码中存在一些对于静态方法的调用,此时需要对静态方法进行模拟
java
try (MockedStatic<FileUtil> ignore2 = mockStatic(FileUtil.class)) {
when(FileUtil.getFileRowCount(any())).thenReturn(100);
assertTrue(pushService.exec(new File("1.txt"))
}
对象模拟
请求响应模拟
使用MockHttpServletRequest、MockHttpServletResponse:
java
MockHttpServletResponse res = new MockHttpServletResponse();
MockHttpServletRequest req = new MockHttpServletRequest();
String logInfo = "qqq";
req.setQueryString("key=" + logInfo);
req.setParameter("key", logInfo);
Assertions.assertDoesNotThrow(() -> service.process(req, res, context));
assertTrue(response.containsHeader(HttpHeaders.CONNECTION));
Meter模拟
因为meter registry有级联调用,因此要mock它返回的Meter:
java
class ApiServiceTest {
@InjectMocks
ApiService apiService;
@Mock
HttpHelper httpHelper;
@Mock
StepMeterRegistry logger;
@Mock
Timer timer;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
when(logger.timer("api.ask", "success", "0", "name", "sample"))
.thenReturn(timer);
}
@Test
void askDsp() {
when(httpHelper.sendPostByte(eq("http://api.com"),
any(), any(), any())).thenReturn(null);
ApiResp<String> res = apiService.ask("sample", null);
assertFalse(res.isSuccess());
verify(timer).record(anyLong(), eq(TimeUnit.MILLISECONDS));
}
}
构造方法模拟
有一些类的方法中,存在一些直接构造新对象的操作,此时,需要模拟构造方法:
java
try (MockedConstruction<Egg> jsch = mockConstruction(
Egg.class, (mock, context) ->
when(mock.doSomething(anyString()).thenReturn("1"))
) {
// test here
}
注意:File
似乎无法被模拟,如果模拟File会导致整个程序错乱。
结果验证
匹配参数
在ArgumentMathers
类中,拥有类似下列静态方法:
java
public static double doubleThat(ArgumentMatcher<Double> matcher) {...}
public static long longThat(ArgumentMatcher<Long> matcher) {...}
其中,真正用来验证的是ArgumentMatcher
类,有一些内置的ArgumentMatcher
类,例如:
java
// 是否某个类
public static <T> T isA(Class<T> type) {
reportMatcher(new InstanceOf(type));
return defaultValue(type);
}
public class InstanceOf implements ArgumentMatcher<Object>, Serializable {
private final Class<?> clazz;
private final String description;
public InstanceOf(Class<?> clazz) {
this(clazz, "isA(" + clazz.getCanonicalName() + ")");
}
public InstanceOf(Class<?> clazz, String describedAs) {
this.clazz = clazz;
this.description = describedAs;
}
@Override
public boolean matches(Object actual) {
return (actual != null)
&& (Primitives.isAssignableFromWrapper(actual.getClass(), clazz)
|| clazz.isAssignableFrom(actual.getClass()));
}
@Override
public String toString() {
return description;
}
}
java
public class EndsWith implements ArgumentMatcher<String>, Serializable {
private final String suffix;
public EndsWith(String suffix) {
this.suffix = suffix;
}
@Override
public boolean matches(String actual) {
return actual != null && actual.endsWith(suffix);
}
@Override
public String toString() {
return "endsWith(\"" + suffix + "\")";
}
}
使用场景:
java
verify(service, atLeastOnce()).add(doubleThat(new GreaterOrEqual<>(1D)));
verify(service).log(isA(Context.class));
也可以自定义:
java
verify(service, atLeastOnce()).log(argThat(context -> "1".equals(context.id));
测试基类
对于同类型的类可以用一个基类提取一些公共方法。
基类:
java
public abstract class BaseTest<T extends BaseService> {
@Spy
protected Data dbData;
@Spy
protected XxService xxService;
@Mock
protected ObjectMapper objectMapper;
@Setter
T service;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
// 初始化数据
initData()
// 整理数据
dbData.organize();
// 通用mock
when(xxService.get("id")).thenReturn(1);
// 初始化Json工具参数
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
protected abstract void initData();
// 公共方法
protected Resp getSomething(String id) {
int id = xxService.get(id);
return new Resp(id);
}
}
子类
java
public class AServiceTest extends BaseServiceTest<AService> {
@InjectMocks
AService aService;
@BeforeEach
void setUp() {
super.setUp();
setService(aService);
}
@Override
protected void initData() {
dbData.getBook().put("book:science", Collections.singleton(100));
dbData.getBook().put("book:art", Collections.singleton(101));
}
@Test
void test() {
String id = service.something();
Resp resp = getSomething(id);
Asserstions.assertEquals("1", service.run(resp));
}
}
无法mock的场景
多种注入方式
如果一个Bean类同时使用了构造函数注入和Field注入,则其Field无法被模拟注入。(字段注入和构造器注入同时存在Mockitio不能mock字段注入的bean_mock注入bean空指针-CSDN博客)
服务类
java
class Service {
@Autowired
Helper helper;
Data data;
public Service(Data data) {
this.data = data
}
}
配置类
java
@Configuration
class Configuration {
@Bean
public Service service(Data data) {
return new Service(data);
}
}
测试类
java
class ServiceTest {
@InjectMock
Service service;
@Mock
Data data;
@Mock
Helper helper;
}
此时无法同时模拟注入data和helper。