Mockito不常用的方法

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。

相关推荐
零千叶16 小时前
【面试】AI大模型应用原理面试题
java·设计模式·面试
坐吃山猪21 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫21 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao21 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区1 天前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT1 天前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy1 天前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss1 天前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续1 天前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升