You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为使用R2DBC DatabaseClient的Spring Boot Repository编写单元测试?

问题

我有如下ItemRepository类:

@Repository
public class ItemRepository {

    @Autowired
    DatabaseClient databaseClient;

    @Autowired
    private Environment queries;

    protected String sql(String key) {
        return queries.getProperty(key);
    }

    public Flux<ItemObj> getValues(List<String> ids, Integer year,
                                                String family, List<String> pIds,
                                                List<String> tIds) {


        return databaseClient.sql(sql("get_values"))
                .bind(ids, Parameter.fromOrEmpty(ids, String.class))
                .bind(year, Parameter.fromOrEmpty(year, Integer.class))
                .bind(family, Parameter.fromOrEmpty(family, String.class))
                .bind(pIds, Parameter.fromOrEmpty(pIds, String.class))
                .bind(tIds, Parameter.fromOrEmpty(tIds, String.class))
                .map(row -> {
                    ItemObj itemObj = new ItemObj();
                    itemObj.setItemName(row.get("item_name", String.class));
                    //rest setters from the response
                    ...
                    ...
                    ...
                    return itemObj;
                })
                .all();
    }

}

其中get_values定义在配置文件中,对应SQL语句。该实现功能正常,但我想为这个类编写单元测试,由于DatabaseClient存在链式调用,没法准确Mock并编写测试用例。请问有没有合适的方法?能否提供示例?


解决方案

针对DatabaseClient链式调用难以Mock的问题,推荐两种可行方案:

方案一:Mockito 链式Mock(单元测试)

利用Mockito的RETURNS_DEEP_STUBS特性,Mock整个DatabaseClient的调用链,同时验证参数绑定和结果转换逻辑。

测试示例

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.r2dbc.core.Parameter;
import org.springframework.r2dbc.core.Row;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;

import java.util.List;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class ItemRepositoryTest {

    // 用RETURNS_DEEP_STUBS支持链式调用的Mock
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private DatabaseClient databaseClient;

    @Mock
    private Environment queries;

    @InjectMocks
    private ItemRepository itemRepository;

    @Test
    void getValues_returnsExpectedItems() {
        // 1. 准备测试参数
        List<String> testIds = List.of("1", "2");
        Integer testYear = 2024;
        String testFamily = "electronics";
        List<String> testPIds = List.of("p1", "p2");
        List<String> testTIds = List.of("t1", "t2");

        // 2. 准备预期返回的Item对象
        ItemObj expectedItem = new ItemObj();
        expectedItem.setItemName("Test Item");

        // 3. Mock配置文件的SQL读取
        when(queries.getProperty("get_values")).thenReturn("SELECT item_name FROM items WHERE ...");

        // 4. Mock Row对象的字段获取逻辑
        Row mockRow = mock(Row.class);
        when(mockRow.get("item_name", String.class)).thenReturn("Test Item");

        // 5. Mock整个DatabaseClient链式调用
        when(databaseClient.sql(anyString())
                .bind(eq("ids"), any(Parameter.class))
                .bind(eq("year"), any(Parameter.class))
                .bind(eq("family"), any(Parameter.class))
                .bind(eq("pIds"), any(Parameter.class))
                .bind(eq("tIds"), any(Parameter.class))
                .map(any()) // 匹配任意转换函数
                .all())
                .thenReturn(Flux.just(expectedItem));

        // 6. 执行测试并验证结果
        Flux<ItemObj> result = itemRepository.getValues(testIds, testYear, testFamily, testPIds, testTIds);

        StepVerifier.create(result)
                .expectNext(expectedItem)
                .verifyComplete();
    }
}

注意点

  • 确保bind()方法的参数名(比如"ids")和你SQL中的占位符完全一致
  • 如果需要更精确的验证,可以替换any()为具体的参数匹配器,比如验证Parameter的实际值

方案二:Testcontainers 集成测试(更可靠)

如果想验证真实SQL的执行逻辑、参数绑定正确性,推荐用Testcontainers启动真实数据库做集成测试,避免Mock带来的逻辑偏差。

测试示例

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.test.StepVerifier;

import java.util.List;

@Testcontainers
@DataR2dbcTest
@Import(ItemRepository.class) // 导入待测试的Repository类
class ItemRepositoryIntegrationTest {

    // 启动PostgreSQL测试容器
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");

    // 动态配置R2DBC连接参数
    @DynamicPropertySource
    static void configureDatabaseProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.r2dbc.url", () -> 
            String.format("r2dbc:postgresql://%s:%d/%s", 
                postgres.getHost(), postgres.getFirstMappedPort(), postgres.getDatabaseName()));
        registry.add("spring.r2dbc.username", postgres::getUsername);
        registry.add("spring.r2dbc.password", postgres::getPassword);
        // 配置测试用的SQL语句(替代配置文件中的get_values)
        registry.add("queries.get_values", () -> 
            "SELECT item_name FROM items WHERE ids = ANY(:ids) AND year = :year AND family = :family AND p_ids = ANY(:pIds) AND t_ids = ANY(:tIds)");
    }

    @Autowired
    private ItemRepository itemRepository;

    // 可选:测试前插入测试数据
    // @BeforeEach
    // void initTestData() {
    //     databaseClient.sql("INSERT INTO items (item_name, ids, year, family, p_ids, t_ids) " +
    //             "VALUES ('Test Item', ARRAY['1','2'], 2024, 'electronics', ARRAY['p1','p2'], ARRAY['t1','t2'])")
    //             .fetch()
    //             .rowsUpdated()
    //             .block();
    // }

    @Test
    void getValues_returnsMatchingItems() {
        // 准备查询参数
        List<String> testIds = List.of("1", "2");
        Integer testYear = 2024;
        String testFamily = "electronics";
        List<String> testPIds = List.of("p1", "p2");
        List<String> testTIds = List.of("t1", "t2");

        // 执行查询并验证结果
        Flux<ItemObj> result = itemRepository.getValues(testIds, testYear, testFamily, testPIds, testTIds);

        StepVerifier.create(result)
                .expectNextMatches(item -> "Test Item".equals(item.getItemName()))
                .verifyComplete();
    }
}

优势

  • 直接用真实数据库执行SQL,能发现SQL语法错误、参数绑定问题
  • 测试逻辑更贴近生产环境,结果更可信

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 01:30:14