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

SpringBoot中如何测试调用多个内部已测方法的业务方法

问题说明

需要对购物车全选业务方法cartSelectAll做单元测试,目标方法逻辑如下:

public CartPriceResponse cartSelectAll(final String check, final User user) {
    List<Cart> carts = cartRepository.findByUser(user);
    List<CartPricePerSellerResponse> cartPricePerSellerResponses = new ArrayList<>();

    for (Cart cart : carts) {
        cart.setIsSelected(check.equals("true"));
        cartPricePerSellerResponses.add(cartPricePerSeller(user, cart.getItem().getSeller()));
    }
    final CartPriceTotalResponse cartPriceTotalResponse = cartPriceTotal(user);

    return CartPriceResponse.builder()
            .cartPricePerSellerResponses(cartPricePerSellerResponses)
            .cartPriceTotalResponse(cartPriceTotalResponse)
            .build();
}

方法核心逻辑:

  • 入参check值为字符串"true"时,将当前用户所有购物车条目的选中状态置为true,否则置为false
  • 遍历购物车条目时调用cartPricePerSeller计算单卖家维度的购物车价格
  • 最后调用cartPriceTotal计算全量购物车总价,组装为CartPriceResponse返回

当前测试前提:已经完成cartPricePerSeller、cartPriceTotal两个方法的独立单元测试,不需要在cartSelectAll测试中重复验证二者内部逻辑,需要明确内部方法调用的Mock/Spy方案。同时现有代码存在设计缺陷,需要可落地的重构指引。

涉及的关联方法代码如下:

public CartPricePerSellerResponse cartPricePerSeller(User user, Seller seller) {
    final List<Cart> carts = cartRepository.findByUserAndIsSelectedTrue(user);

    final List<Cart> cartList = carts.stream().filter(cart -> cart.getItem().getSeller().equals(seller)).collect(Collectors.toList());
    long cartTotalPricePerSeller = 0;
    int cartTotalQuantityPerSeller = 0;

    for (Cart cart : cartList) {
        cartTotalPricePerSeller += cart.getItemOption().getItemPrice().getPriceNow() * cart.getQuantity();
        cartTotalQuantityPerSeller += cart.getQuantity();
    }
    return CartPricePerSellerResponse.builder()
            .sellerId(seller.getId())
            .itemTotalPricePerSeller(cartTotalPricePerSeller)
            .itemTotalQuantityPerSeller(cartTotalQuantityPerSeller)
            .itemShippingFeePerSeller(shippingFeeCheck(seller.getId(), cartTotalPricePerSeller))
            .build();
}

public CartPriceTotalResponse cartPriceTotal(User user) {
    final List<Cart> carts = cartRepository.findByUserAndIsSelectedTrue(user);

    long cartTotalPrice = 0;
    int cartTotalQuantity = 0;
    int cartTotalShippingFee = 0;

    final List<Seller> sellers = carts.stream().map(Cart::getItem).map(Item::getSeller).distinct().collect(Collectors.toList());
    for (Seller seller : sellers) {
        final CartPricePerSellerResponse cartPricePerSellerResponse = this.cartPricePerSeller(user, seller);

        cartTotalPrice += cartPricePerSellerResponse.getItemTotalPricePerSeller();
        cartTotalQuantity += cartPricePerSellerResponse.getItemTotalQuantityPerSeller();
        cartTotalShippingFee += cartPricePerSellerResponse.getItemShippingFeePerSeller();
    }
    return CartPriceTotalResponse.builder()
            .cartTotalPrice(cartTotalPrice)
            .cartTotalQuantity(cartTotalQuantity)
            .cartTotalShippingFee(cartTotalShippingFee)
            .build();
}

单元测试Spy/Mock实现方案

三个方法都在同一个类中,不能直接用普通Mock注入,需要用@Spy注解部分模拟被测类实例,只打桩内部依赖的方法,保留cartSelectAll本身的真实逻辑:

  • 用@Mock模拟cartRepository这类外部依赖
  • 用@InjectMocks+@Spy标注被测服务实例,保证同类内部方法调用可以被打桩
  • 固定cartRepository.findByUser的返回值为提前构造的测试购物车数据
  • 对cartPricePerSeller、cartPriceTotal方法做打桩,返回提前构造的Mock响应,不需要执行方法真实逻辑
  • 核心验证点:
    • 传入check="true"时,所有购物车条目的isSelected属性都被设置为true;传入其他值时,所有isSelected属性被设置为false
    • 遍历每个购物车条目所属卖家时,都调用了一次cartPricePerSeller,入参为当前用户和对应卖家
    • 最后调用了一次cartPriceTotal,入参为当前用户
    • 返回结果正确组装了两个方法打桩返回的响应数据

基于Mockito的示例测试代码片段:

@ExtendWith(MockitoExtension.class)
class CartServiceTest {
    @Mock
    private CartRepository cartRepository;
    @Spy
    @InjectMocks
    private CartService cartService;

    @Test
    void cartSelectAll_shouldSetSelectedAndAssembleResponse_whenCheckIsTrue() {
        // 构造测试数据
        User testUser = new User(1L);
        Seller seller1 = new Seller(1L);
        Seller seller2 = new Seller(2L);
        Cart cart1 = Cart.builder().user(testUser).item(Item.builder().seller(seller1).build()).build();
        Cart cart2 = Cart.builder().user(testUser).item(Item.builder().seller(seller2).build()).build();
        List<Cart> userCarts = List.of(cart1, cart2);
        // 构造Mock返回
        CartPricePerSellerResponse sellerResp1 = CartPricePerSellerResponse.builder().sellerId(1L).build();
        CartPricePerSellerResponse sellerResp2 = CartPricePerSellerResponse.builder().sellerId(2L).build();
        CartPriceTotalResponse totalResp = CartPriceTotalResponse.builder().cartTotalPrice(1000L).build();

        // 打桩
        when(cartRepository.findByUser(testUser)).thenReturn(userCarts);
        // 打桩同类内部方法,跳过真实逻辑
        doReturn(sellerResp1).when(cartService).cartPricePerSeller(testUser, seller1);
        doReturn(sellerResp2).when(cartService).cartPricePerSeller(testUser, seller2);
        doReturn(totalResp).when(cartService).cartPriceTotal(testUser);

        // 执行方法
        CartPriceResponse result = cartService.cartSelectAll("true", testUser);

        // 验证逻辑
        assertTrue(cart1.getIsSelected());
        assertTrue(cart2.getIsSelected());
        verify(cartService, times(1)).cartPricePerSeller(testUser, seller1);
        verify(cartService, times(1)).cartPricePerSeller(testUser, seller2);
        verify(cartService, times(1)).cartPriceTotal(testUser);
        assertEquals(2, result.getCartPricePerSellerResponses().size());
        assertEquals(totalResp, result.getCartPriceTotalResponse());
    }
}

代码重构方案

现有代码的核心问题是职责耦合、重复查询、逻辑分层不清:

  • 价格计算逻辑和购物车状态修改逻辑耦合在同一个服务类中
  • cartPricePerSeller每次调用都会全量查询当前用户所有选中的购物车条目,循环调用时会产生N次重复数据库查询
  • 运费计算逻辑shippingFeeCheck直接硬编码在方法内部,后续运费规则调整需要修改核心价格计算逻辑
  • cartPriceTotal内部循环调用cartPricePerSeller,如果上层方法也循环调用cartPricePerSeller会产生重复计算

重构按分层拆分职责即可:

  • 拆分独立的价格计算组件:新建CartPriceCalculator类,将cartPricePerSeller、cartPriceTotal、shippingFeeCheck方法迁移到这个独立组件中,和购物车状态修改的逻辑解耦。迁移后cartSelectAll中直接注入CartPriceCalculator调用对应方法,此时单元测试可以直接Mock计算器组件,不需要再用Spy处理同类调用
  • 消除重复数据库查询:调整价格计算方法入参,不要在方法内部查询购物车数据,上层方法先一次性查询出需要的购物车列表,作为入参传给价格计算方法,避免循环内重复查库
  • 拆分运费计算规则:新建ShippingFeeStrategy接口,将shippingFeeCheck的逻辑抽象为策略实现,后续新增运费规则只需要新增策略实现类,不需要修改购物车价格计算核心逻辑
  • 优化全选方法逻辑:重构后cartSelectAll逻辑简化为:查询用户所有购物车→批量更新选中状态→一次性把购物车列表传给价格计算器,拿到卖家维度价格和总价→组装返回,全程只有1次购物车查询,没有重复计算。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 09:54:24