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

Spring Boot如何测试分页返回结果及编写规范的Service层单元测试

现有测试写法存在的问题
  • 核心逻辑错误:不应该Mock被测类ProductServiceImpl本身的方法。你当前对getAllProducts和saveProduct的Mock操作,会导致完全不执行Service的真实业务代码,测试没有任何意义。你现在觉得save测试能通过纯属巧合,就算你删除repository.save(product)这行业务代码,测试依然会返回成功。
  • 依赖Mock方向错误:分页测试中你注释掉的when(productRepository.findProductByMerchantId(...))才是正确方向,我们只需要Mock Service的外部依赖ProductRepository的返回,不需要动被测类本身。
  • 缺少必要断言:现有测试只验证了Repository方法被调用,没有验证Service返回的结果是否符合预期,也没有覆盖异常分支的场景。
  • 逻辑验证不完整:没有校验传入的分页、排序参数是否被正确转换成Pageable对象传递给Repository。
规范的Service层单元测试写法

Service层单元测试的核心原则是:只验证Service本身的业务逻辑,所有外部依赖(这里就是ProductRepository)全部用Mock对象替代,验证范围包括入参处理逻辑、依赖调用的参数正确性、返回值是否符合预期、异常是否正确抛出。

修正后的完整测试代码如下:

@ExtendWith(MockitoExtension.class)
class ProductServiceImplTest {

    @Mock
    ProductRepository productRepository;

    @InjectMocks
    ProductServiceImpl productServiceImpl;

    @Test
    void getAllProducts_正常返回分页结果() {
        // 构造测试数据
        Product product = new Product();
        List<Product> productList = Collections.singletonList(product);
        Page<Product> mockPage = new PageImpl<>(productList);
        Long merchantId = 2L;
        Integer pageNo = 0;
        Integer pageSize = 2;
        String sortBy = "inventory";

        // Mock依赖返回
        when(productRepository.findProductByMerchantId(eq(merchantId), argThat(pageable ->
                pageable.getPageNumber() == pageNo
                        && pageable.getPageSize() == pageSize
                        && pageable.getSort().getOrderFor(sortBy) != null
        ))).thenReturn(Optional.of(mockPage));

        // 调用被测方法
        List<Product> result = productServiceImpl.getAllProducts(merchantId, pageNo, pageSize, sortBy);

        // 断言结果
        assertNotNull(result);
        assertEquals(1, result.size());
        assertEquals(product, result.get(0));
        // 验证依赖调用次数
        verify(productRepository, times(1)).findProductByMerchantId(any(Long.class), any(Pageable.class));
    }

    @Test
    void getAllProducts_无对应商户产品时抛出异常() {
        // 构造入参
        Long merchantId = 999L;

        // Mock返回空Optional触发异常
        when(productRepository.findProductByMerchantId(eq(merchantId), any(Pageable.class)))
                .thenReturn(Optional.empty());

        // 断言异常是否符合预期
        IllegalStateException exception = assertThrows(IllegalStateException.class,
                () -> productServiceImpl.getAllProducts(merchantId, 0, 2, "inventory"));
        assertTrue(exception.getMessage().contains(merchantId.toString()));

        // 验证依赖调用
        verify(productRepository, times(1)).findProductByMerchantId(eq(merchantId), any(Pageable.class));
    }

    @Test
    void saveProduct_保存成功返回产品() {
        // 构造入参
        Product product = new Product();

        // 直接调用被测方法,不需要Mock被测类本身
        Product result = productServiceImpl.saveProduct(product);

        // 断言结果
        assertNotNull(result);
        assertEquals(product, result);

        // 验证Repository被正确调用
        verify(productRepository, times(1)).save(product);
    }
}
Repository接口是否需要测试

需要,但不属于单元测试范畴,属于集成测试:

  • Repository层是直接和数据库交互的层,需要启动Spring上下文,连接测试数据库(一般用H2内存数据库降低测试成本)来验证自定义查询的逻辑正确性。
  • 你定义的findProductByMerchantId的分页、排序逻辑是否正确,只有集成测试可以验证,不要把这部分测试耦合到Service层的单元测试中,两者测试边界要清晰。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.12 19:42:03