如何为使用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
相关产品推荐
相关产品推荐

