You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何Mock Elasticsearch行为并编写JUnit单元测试

用JUnit + Mockito测试Elasticsearch持久化功能的指南

嘿,刚上手JUnit测试ES持久化是吧?别担心,我帮你一步步拆解怎么Mock ES的行为,写出精准的单元测试——毕竟单元测试的核心是测你的代码逻辑,而不是真的连ES集群

先明确核心思路

你的场景里,实现类大概率依赖了ES的客户端(比如官方的RestHighLevelClient),我们要做的就是:

  • Mock这个ES客户端,让它返回我们预设的响应
  • 验证你的实现类是否正确调用了ES的API(比如index/bulk方法)
  • 验证传入的参数(比如文档内容、索引名)是否符合预期

第一步:准备依赖

确保你的项目里有JUnit 5和Mockito的依赖,以Maven为例:

<dependencies>
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.9.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.9.2</version>
        <scope>test</scope>
    </dependency>
    <!-- Mockito -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>4.11.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>4.11.0</version>
        <scope>test</scope>
    </dependency>
    <!-- Elasticsearch 客户端依赖(主项目里已有,测试时不用改scope) -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.17.9</version>
    </dependency>
</dependencies>

第二步:示例代码准备

先假设你的接口和实现类大概是这样:

持久化接口

public interface ElasticsearchDataPersister<T> {
    boolean persist(T data, String indexName) throws IOException;
}

示例实现类

import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.rest.RestStatus;

import java.io.IOException;

public class DefaultElasticsearchPersister<T> implements ElasticsearchDataPersister<T> {

    private final RestHighLevelClient esClient;
    private final ObjectMapper objectMapper;

    // 构造注入客户端和JSON序列化工具
    public DefaultElasticsearchPersister(RestHighLevelClient esClient, ObjectMapper objectMapper) {
        this.esClient = esClient;
        this.objectMapper = objectMapper;
    }

    @Override
    public boolean persist(T data, String indexName) throws IOException {
        IndexRequest request = new IndexRequest(indexName)
                .source(objectMapper.writeValueAsString(data), XContentType.JSON);
        IndexResponse response = esClient.index(request, RequestOptions.DEFAULT);
        // 假设返回成功状态则返回true
        return RestStatus.CREATED.equals(response.status());
    }
}

第三步:编写单元测试

我们用Mockito来MockRestHighLevelClient,验证实现类的逻辑:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.rest.RestStatus;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class) // 启用Mockito的JUnit 5扩展
class DefaultElasticsearchPersisterTest {

    @Mock // Mock ES客户端
    private RestHighLevelClient mockEsClient;

    @Mock // Mock JSON序列化工具(如果你的实现里用到的话)
    private ObjectMapper mockObjectMapper;

    @InjectMocks // 自动把Mock对象注入到实现类里
    private DefaultElasticsearchPersister<User> persister;

    @Captor // 用来捕获传入ES的IndexRequest参数,方便验证
    private ArgumentCaptor<IndexRequest> indexRequestCaptor;

    // 测试成功持久化的场景
    @Test
    void persist_ShouldReturnTrue_WhenEsResponseIsCreated() throws IOException {
        // 1. 准备测试数据
        User testUser = new User("1", "Alice", 25);
        String testIndex = "users";
        String mockJson = "{\"id\":\"1\",\"name\":\"Alice\",\"age\":25}";

        // 2. Mock JSON序列化和ES客户端的响应
        when(mockObjectMapper.writeValueAsString(testUser)).thenReturn(mockJson);
        IndexResponse mockResponse = mock(IndexResponse.class);
        when(mockResponse.status()).thenReturn(RestStatus.CREATED);
        when(mockEsClient.index(any(IndexRequest.class), eq(RequestOptions.DEFAULT)))
                .thenReturn(mockResponse);

        // 3. 调用被测试的方法
        boolean result = persister.persist(testUser, testIndex);

        // 4. 验证结果
        assertTrue(result);
        // 验证ES客户端的index方法被调用了一次
        verify(mockEsClient, times(1)).index(indexRequestCaptor.capture(), eq(RequestOptions.DEFAULT));

        // 5. 捕获并验证传入的参数是否正确
        IndexRequest capturedRequest = indexRequestCaptor.getValue();
        assertEquals(testIndex, capturedRequest.index());
        assertEquals(mockJson, capturedRequest.source().utf8ToString());
    }

    // 测试ES抛出异常的场景
    @Test
    void persist_ShouldThrowIOException_WhenEsClientThrows() throws IOException {
        // 1. 准备测试数据
        User testUser = new User("2", "Bob", 30);
        String testIndex = "users";
        String mockJson = "{\"id\":\"2\",\"name\":\"Bob\",\"age\":30}";

        // 2. Mock JSON序列化,然后让ES客户端抛出IOException
        when(mockObjectMapper.writeValueAsString(testUser)).thenReturn(mockJson);
        when(mockEsClient.index(any(IndexRequest.class), eq(RequestOptions.DEFAULT)))
                .thenThrow(new IOException("ES connection failed"));

        // 3. 验证调用方法时是否抛出了预期的异常
        IOException exception = assertThrows(IOException.class,
                () -> persister.persist(testUser, testIndex));
        assertEquals("ES connection failed", exception.getMessage());

        // 4. 验证ES客户端的index方法被调用了一次
        verify(mockEsClient, times(1)).index(any(), eq(RequestOptions.DEFAULT));
    }

    // 测试用的User类
    static class User {
        private String id;
        private String name;
        private int age;

        public User(String id, String name, int age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        // getter方法省略
    }
}

关键技巧说明

  1. @Mock@InjectMocks:Mockito会自动创建Mock对象,并注入到被测试类的构造器或字段中,省去手动实例化的麻烦。
  2. ArgumentCaptor:用来捕获传入Mock方法的参数,方便你验证参数的正确性(比如索引名、文档内容),这是测试持久化逻辑的关键——确保你传给ES的内容是对的。
  3. Mock异常场景:不要只测成功的情况,也要测试ES客户端抛出异常时,你的代码是否正确处理(比如抛出异常、返回错误标识)。
  4. 不要测试ES本身:单元测试的目标是验证你的代码是否正确调用了ES API,ES的功能(比如文档是否真的被存储)应该用集成测试(比如用TestContainers启动一个临时ES集群)来验证。

如果你用的是Spring Data Elasticsearch

如果你的实现类依赖的是ElasticsearchOperations或者ElasticsearchRepository,那Mock会更简单——直接Mock这些Spring Data的接口,不用管底层的ES客户端:

@Mock
private ElasticsearchOperations mockEsOps;

@InjectMocks
private SpringDataEsPersister persister;

@Test
void persist_ShouldSaveData() {
    User testUser = new User("1", "Alice", 25);
    when(mockEsOps.save(testUser)).thenReturn(testUser);

    boolean result = persister.persist(testUser);

    assertTrue(result);
    verify(mockEsOps, times(1)).save(testUser);
}

内容的提问来源于stack exchange,提问作者wandermonk

火山引擎 最新活动