私有方法单元测试全面指南

私有方法单元测试全面指南

1. 私有方法测试的正确思路

核心原则:不要直接测试私有方法

私有方法是实现细节,应该通过公共接口来测试其功能。如果私有方法复杂到需要单独测试,说明它应该被提取到单独的类中。

2. 私有方法测试的几种策略

策略1:通过公共方法测试(推荐)

/**
 * 用户服务类 - 包含私有方法的示例
 */
public class UserService {
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    
    public UserService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
        this.passwordEncoder = passwordEncoder;
        this.userRepository = userRepository;
    }
    
    /**
     * 公共方法 - 用户注册
     * 内部会调用多个私有方法
     */
    public User registerUser(UserRegistrationRequest request) {
        // 调用私有方法验证输入
        validateRegistrationRequest(request);
        
        // 调用私有方法加密密码
        String encryptedPassword = encryptPassword(request.getPassword());
        
        // 调用私有方法创建用户实体
        User user = createUserEntity(request, encryptedPassword);
        
        // 保存用户
        return userRepository.save(user);
    }
    
    /**
     * 私有方法1: 验证注册请求
     */
    private void validateRegistrationRequest(UserRegistrationRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("注册请求不能为空");
        }
        if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        if (request.getPassword() == null || request.getPassword().length() < 6) {
            throw new IllegalArgumentException("密码长度至少6位");
        }
        if (request.getEmail() == null || !isValidEmail(request.getEmail())) {
            throw new IllegalArgumentException("邮箱格式不正确");
        }
    }
    
    /**
     * 私有方法2: 验证邮箱格式
     */
    private boolean isValidEmail(String email) {
        // 简单的邮箱验证逻辑
        return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }
    
    /**
     * 私有方法3: 加密密码
     */
    private String encryptPassword(String plainPassword) {
        return passwordEncoder.encode(plainPassword);
    }
    
    /**
     * 私有方法4: 创建用户实体
     */
    private User createUserEntity(UserRegistrationRequest request, String encryptedPassword) {
        return User.builder()
                .username(request.getUsername())
                .email(request.getEmail())
                .password(encryptedPassword)
                .status(UserStatus.ACTIVE)
                .createdAt(LocalDateTime.now())
                .build();
    }
    
    /**
     * 公共方法 - 密码强度验证(暴露部分私有逻辑)
     */
    public boolean isPasswordStrong(String password) {
        return validatePasswordStrength(password);
    }
    
    /**
     * 私有方法5: 验证密码强度
     */
    private boolean validatePasswordStrength(String password) {
        if (password == null || password.length() < 8) {
            return false;
        }
        // 检查包含数字
        boolean hasDigit = password.chars().anyMatch(Character::isDigit);
        // 检查包含字母
        boolean hasLetter = password.chars().anyMatch(Character::isLetter);
        // 检查包含特殊字符
        boolean hasSpecial = password.chars().anyMatch(ch -> !Character.isLetterOrDigit(ch));
        
        return hasDigit && hasLetter && hasSpecial;
    }
}

通过公共方法测试私有方法的单元测试

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;

/**
 * 通过公共方法测试私有方法的单元测试
 * 这是推荐的做法
 */
@ExtendWith(MockitoExtension.class)
class UserServicePublicInterfaceTest {

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    private UserRegistrationRequest validRequest;

    @BeforeEach
    void setUp() {
        validRequest = UserRegistrationRequest.builder()
                .username("testuser")
                .email("test@example.com")
                .password("strongPassword123!")
                .build();
    }

    /**
     * 测试私有方法 validateRegistrationRequest 的各种验证逻辑
     * 通过注册方法的异常情况来测试
     */
    @Test
    @DisplayName("注册用户 - 空请求时应抛出异常(测试私有验证逻辑)")
    void registerUser_WithNullRequest_ShouldThrowException() {
        // 这个测试实际上验证了私有方法 validateRegistrationRequest 的空值检查逻辑
        assertThatThrownBy(() -> userService.registerUser(null))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("注册请求不能为空");
    }

    @Test
    @DisplayName("注册用户 - 用户名为空时应抛出异常(测试私有验证逻辑)")
    void registerUser_WithEmptyUsername_ShouldThrowException() {
        UserRegistrationRequest request = UserRegistrationRequest.builder()
                .username("")  // 空用户名
                .email("test@example.com")
                .password("password123")
                .build();

        assertThatThrownBy(() -> userService.registerUser(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("用户名不能为空");
    }

    @Test
    @DisplayName("注册用户 - 密码过短时应抛出异常(测试私有验证逻辑)")
    void registerUser_WithShortPassword_ShouldThrowException() {
        UserRegistrationRequest request = UserRegistrationRequest.builder()
                .username("testuser")
                .email("test@example.com")
                .password("123")  // 密码太短
                .build();

        assertThatThrownBy(() -> userService.registerUser(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("密码长度至少6位");
    }

    @Test
    @DisplayName("注册用户 - 无效邮箱时应抛出异常(测试私有邮箱验证逻辑)")
    void registerUser_WithInvalidEmail_ShouldThrowException() {
        UserRegistrationRequest request = UserRegistrationRequest.builder()
                .username("testuser")
                .email("invalid-email")  // 无效邮箱格式
                .password("password123")
                .build();

        assertThatThrownBy(() -> userService.registerUser(request))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("邮箱格式不正确");
    }

    /**
     * 测试私有方法 isValidEmail 的逻辑
     * 通过公共方法中的异常来间接测试
     */
    @ParameterizedTest
    @DisplayName("注册用户 - 各种邮箱格式验证(测试私有邮箱验证逻辑)")
    @CsvSource({
            "valid@example.com, true",      // 有效邮箱
            "invalid-email, false",         // 无效邮箱
            "test.user@domain.co.uk, true", // 带点的邮箱
            "user@sub.domain.com, true",    // 子域名
            "plainstring, false",           // 普通字符串
            "'', false",                    // 空字符串
            "null, false"                   // null值
    })
    void registerUser_WithVariousEmails_ShouldValidateCorrectly(String email, boolean expectedValid) {
        if ("null".equals(email)) {
            email = null;
        }

        UserRegistrationRequest request = UserRegistrationRequest.builder()
                .username("testuser")
                .email(email)
                .password("password123")
                .build();

        if (expectedValid) {
            // 设置Mock行为
            given(passwordEncoder.encode(anyString())).willReturn("encodedPassword");
            given(userRepository.save(any(User.class))).willAnswer(invocation -> invocation.getArgument(0));
            
            // 应该成功执行
            User result = userService.registerUser(request);
            assertThat(result).isNotNull();
        } else {
            // 应该抛出异常
            assertThatThrownBy(() -> userService.registerUser(request))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage("邮箱格式不正确");
        }
    }

    /**
     * 测试私有方法 encryptPassword 被正确调用
     */
    @Test
    @DisplayName("注册用户 - 应正确加密密码(测试私有加密方法)")
    void registerUser_ShouldEncryptPassword() {
        // Given
        given(passwordEncoder.encode("strongPassword123!")).willReturn("encodedSecurePassword");
        given(userRepository.save(any(User.class))).willAnswer(invocation -> invocation.getArgument(0));

        // When
        User result = userService.registerUser(validRequest);

        // Then
        // 验证密码编码器被调用,说明私有方法 encryptPassword 工作了
        verify(passwordEncoder).encode("strongPassword123!");
        // 验证保存的用户使用了加密后的密码
        assertThat(result.getPassword()).isEqualTo("encodedSecurePassword");
    }

    /**
     * 测试私有方法 createUserEntity 创建的用户对象结构
     */
    @Test
    @DisplayName("注册用户 - 应创建正确的用户实体(测试私有实体创建方法)")
    void registerUser_ShouldCreateCorrectUserEntity() {
        // Given
        given(passwordEncoder.encode(anyString())).willReturn("encodedPassword");
        given(userRepository.save(any(User.class))).willAnswer(invocation -> invocation.getArgument(0));

        // When
        User result = userService.registerUser(validRequest);

        // Then
        // 验证私有方法 createUserEntity 创建的用户对象属性正确
        assertThat(result.getUsername()).isEqualTo("testuser");
        assertThat(result.getEmail()).isEqualTo("test@example.com");
        assertThat(result.getPassword()).isEqualTo("encodedPassword");
        assertThat(result.getStatus()).isEqualTo(UserStatus.ACTIVE);
        assertThat(result.getCreatedAt()).isNotNull();
    }

    /**
     * 通过公共方法 isPasswordStrong 测试私有方法 validatePasswordStrength
     */
    @ParameterizedTest
    @DisplayName("密码强度验证 - 各种密码情况(直接测试暴露的私有逻辑)")
    @CsvSource({
            "Short1!, false",           // 长度不足8位
            "longpassword, false",      // 只有字母
            "123456789, false",         // 只有数字
            '!@#$%^&*, false',          // 只有特殊字符
            "weakPass, false",          // 字母+数字,但无特殊字符
            "Strong123, false",         // 字母+数字,但无特殊字符
            "Strong123!, true",         // 强密码:字母+数字+特殊字符
            "Another$123, true",        // 另一个强密码
            "'', false",                // 空密码
            "null, false"               // null密码
    })
    void isPasswordStrong_WithVariousPasswords_ShouldReturnCorrectResult(String password, boolean expectedStrong) {
        if ("null".equals(password)) {
            password = null;
        }

        boolean result = userService.isPasswordStrong(password);

        assertThat(result).isEqualTo(expectedStrong);
    }
}

策略2:重构提取私有方法(强烈推荐)

/**
 * 重构后的代码 - 将复杂的私有逻辑提取到专门的类中
 */
public class RefactoredUserService {
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    private final UserValidator userValidator;
    private final UserFactory userFactory;
    
    public RefactoredUserService(PasswordEncoder passwordEncoder, 
                               UserRepository userRepository,
                               UserValidator userValidator,
                               UserFactory userFactory) {
        this.passwordEncoder = passwordEncoder;
        this.userRepository = userRepository;
        this.userValidator = userValidator;
        this.userFactory = userFactory;
    }
    
    public User registerUser(UserRegistrationRequest request) {
        // 使用专门的验证器
        userValidator.validateRegistrationRequest(request);
        
        // 创建用户实体
        User user = userFactory.createUser(request, passwordEncoder);
        
        return userRepository.save(user);
    }
}

/**
 * 用户验证器 - 原来私有方法的逻辑现在可以单独测试
 */
@Component
public class UserValidator {
    
    public void validateRegistrationRequest(UserRegistrationRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("注册请求不能为空");
        }
        if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        if (request.getPassword() == null || request.getPassword().length() < 6) {
            throw new IllegalArgumentException("密码长度至少6位");
        }
        if (request.getEmail() == null || !isValidEmail(request.getEmail())) {
            throw new IllegalArgumentException("邮箱格式不正确");
        }
    }
    
    public boolean isValidEmail(String email) {
        return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }
    
    public boolean validatePasswordStrength(String password) {
        if (password == null || password.length() < 8) {
            return false;
        }
        boolean hasDigit = password.chars().anyMatch(Character::isDigit);
        boolean hasLetter = password.chars().anyMatch(Character::isLetter);
        boolean hasSpecial = password.chars().anyMatch(ch -> !Character.isLetterOrDigit(ch));
        
        return hasDigit && hasLetter && hasSpecial;
    }
}

/**
 * 用户工厂 - 创建用户实体的逻辑
 */
@Component
public class UserFactory {
    
    public User createUser(UserRegistrationRequest request, PasswordEncoder passwordEncoder) {
        String encryptedPassword = passwordEncoder.encode(request.getPassword());
        
        return User.builder()
                .username(request.getUsername())
                .email(request.getEmail())
                .password(encryptedPassword)
                .status(UserStatus.ACTIVE)
                .createdAt(LocalDateTime.now())
                .build();
    }
}

提取后类的单元测试

/**
 * 用户验证器的单元测试 - 现在可以轻松测试所有逻辑
 */
class UserValidatorTest {

    private UserValidator userValidator;

    @BeforeEach
    void setUp() {
        userValidator = new UserValidator();
    }

    @Test
    @DisplayName("验证注册请求 - 有效请求时应通过")
    void validateRegistrationRequest_WithValidRequest_ShouldPass() {
        UserRegistrationRequest validRequest = UserRegistrationRequest.builder()
                .username("validuser")
                .email("valid@example.com")
                .password("password123")
                .build();

        // 不应该抛出异常
        assertThatCode(() -> userValidator.validateRegistrationRequest(validRequest))
                .doesNotThrowAnyException();
    }

    @Test
    @DisplayName("验证注册请求 - 空请求时应抛出异常")
    void validateRegistrationRequest_WithNullRequest_ShouldThrowException() {
        assertThatThrownBy(() -> userValidator.validateRegistrationRequest(null))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("注册请求不能为空");
    }

    @ParameterizedTest
    @DisplayName("邮箱验证 - 各种邮箱格式")
    @CsvSource({
            "test@example.com, true",
            "user.name@domain.com, true", 
            "user@sub.domain.com, true",
            "invalid-email, false",
            "plainstring, false",
            "'', false",
            "null, false"
    })
    void isValidEmail_WithVariousEmails_ShouldReturnCorrectResult(String email, boolean expected) {
        if ("null".equals(email)) {
            email = null;
        }

        boolean result = userValidator.isValidEmail(email);

        assertThat(result).isEqualTo(expected);
    }

    @ParameterizedTest
    @DisplayName("密码强度验证 - 各种密码情况")
    @CsvSource({
            "Short1!, false",
            "longpassword, false", 
            "12345678, false",
            "Strong123!, true",
            "Weak123, false",
            "'', false",
            "null, false"
    })
    void validatePasswordStrength_WithVariousPasswords_ShouldReturnCorrectResult(String password, boolean expected) {
        if ("null".equals(password)) {
            password = null;
        }

        boolean result = userValidator.validatePasswordStrength(password);

        assertThat(result).isEqualTo(expected);
    }
}

策略3:使用反射测试私有方法(不推荐,仅作了解)

import java.lang.reflect.Method;

/**
 * 使用反射测试私有方法 - 不推荐在生产代码中使用
 * 仅用于理解原理或处理遗留代码
 */
class UserServiceReflectionTest {

    private UserService userService;
    private Method validateEmailMethod;
    private Method validatePasswordStrengthMethod;

    @BeforeEach
    void setUp() throws Exception {
        userService = new UserService(new SimplePasswordEncoder(), mock(UserRepository.class));
        
        // 通过反射获取私有方法
        validateEmailMethod = UserService.class.getDeclaredMethod("isValidEmail", String.class);
        validateEmailMethod.setAccessible(true); // 设置可访问
        
        validatePasswordStrengthMethod = UserService.class.getDeclaredMethod("validatePasswordStrength", String.class);
        validatePasswordStrengthMethod.setAccessible(true);
    }

    @Test
    @DisplayName("反射测试 - 邮箱验证私有方法")
    void testPrivateIsValidEmailWithReflection() throws Exception {
        // 使用反射调用私有方法
        Boolean result1 = (Boolean) validateEmailMethod.invoke(userService, "test@example.com");
        Boolean result2 = (Boolean) validateEmailMethod.invoke(userService, "invalid-email");
        
        assertThat(result1).isTrue();
        assertThat(result2).isFalse();
    }

    @Test
    @DisplayName("反射测试 - 密码强度验证私有方法") 
    void testPrivateValidatePasswordStrengthWithReflection() throws Exception {
        Boolean result1 = (Boolean) validatePasswordStrengthMethod.invoke(userService, "Strong123!");
        Boolean result2 = (Boolean) validatePasswordStrengthMethod.invoke(userService, "weak");
        
        assertThat(result1).isTrue();
        assertThat(result2).isFalse();
    }
}

// 简单的密码编码器实现
class SimplePasswordEncoder extends PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return "encoded_" + rawPassword;
    }
    
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encode(rawPassword).equals(encodedPassword);
    }
}

3. 综合性实际开发示例

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.within;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;

/**
 * 订单价格计算服务 - 包含复杂私有方法的实际案例
 */
public class OrderPricingService {
    
    private final TaxCalculator taxCalculator;
    private final DiscountService discountService;
    
    public OrderPricingService(TaxCalculator taxCalculator, DiscountService discountService) {
        this.taxCalculator = taxCalculator;
        this.discountService = discountService;
    }
    
    /**
     * 公共方法:计算订单总价
     * 内部调用多个私有方法完成复杂计算
     */
    public OrderPrice calculateTotalPrice(Order order, String promoCode) {
        // 验证订单
        validateOrder(order);
        
        // 计算商品小计
        BigDecimal subtotal = calculateSubtotal(order);
        
        // 应用折扣
        BigDecimal discountAmount = calculateDiscount(subtotal, promoCode, order.getCustomer());
        
        // 计算折扣后金额
        BigDecimal amountAfterDiscount = subtotal.subtract(discountAmount);
        
        // 计算税费
        BigDecimal taxAmount = calculateTax(amountAfterDiscount, order.getShippingAddress());
        
        // 计算运费
        BigDecimal shippingCost = calculateShippingCost(order, amountAfterDiscount);
        
        // 计算最终总价
        BigDecimal totalAmount = amountAfterDiscount.add(taxAmount).add(shippingCost);
        
        return OrderPrice.builder()
                .subtotal(subtotal)
                .discountAmount(discountAmount)
                .amountAfterDiscount(amountAfterDiscount)
                .taxAmount(taxAmount)
                .shippingCost(shippingCost)
                .totalAmount(totalAmount)
                .build();
    }
    
    /**
     * 私有方法1: 验证订单
     */
    private void validateOrder(Order order) {
        if (order == null) {
            throw new IllegalArgumentException("订单不能为空");
        }
        if (order.getItems() == null || order.getItems().isEmpty()) {
            throw new IllegalArgumentException("订单商品不能为空");
        }
        for (OrderItem item : order.getItems()) {
            if (item.getQuantity() <= 0) {
                throw new IllegalArgumentException("商品数量必须大于0");
            }
            if (item.getUnitPrice() == null || item.getUnitPrice().compareTo(BigDecimal.ZERO) < 0) {
                throw new IllegalArgumentException("商品价格必须大于等于0");
            }
        }
    }
    
    /**
     * 私有方法2: 计算商品小计
     */
    private BigDecimal calculateSubtotal(Order order) {
        return order.getItems().stream()
                .map(item -> item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    /**
     * 私有方法3: 计算折扣
     */
    private BigDecimal calculateDiscount(BigDecimal subtotal, String promoCode, Customer customer) {
        BigDecimal discountRate = BigDecimal.ZERO;
        
        // 会员折扣
        if (customer.isVIP()) {
            discountRate = discountRate.add(new BigDecimal("0.05")); // VIP 5%折扣
        }
        
        // 促销码折扣
        if (promoCode != null) {
            BigDecimal promoDiscount = discountService.getDiscountRate(promoCode);
            discountRate = discountRate.add(promoDiscount);
        }
        
        // 批量购买折扣
        if (subtotal.compareTo(new BigDecimal("1000")) > 0) {
            discountRate = discountRate.add(new BigDecimal("0.03")); // 大额订单3%折扣
        }
        
        // 折扣率上限30%
        discountRate = discountRate.min(new BigDecimal("0.30"));
        
        return subtotal.multiply(discountRate);
    }
    
    /**
     * 私有方法4: 计算税费
     */
    private BigDecimal calculateTax(BigDecimal amount, Address address) {
        return taxCalculator.calculateTax(amount, address);
    }
    
    /**
     * 私有方法5: 计算运费
     */
    private BigDecimal calculateShippingCost(Order order, BigDecimal amountAfterDiscount) {
        BigDecimal baseShipping = new BigDecimal("15.00");
        
        // 金额超过200免运费
        if (amountAfterDiscount.compareTo(new BigDecimal("200")) > 0) {
            return BigDecimal.ZERO;
        }
        
        // 重量附加费
        BigDecimal weightSurcharge = calculateWeightSurcharge(order.getTotalWeight());
        
        // 紧急配送附加费
        BigDecimal expressSurcharge = order.isExpressShipping() ? new BigDecimal("10.00") : BigDecimal.ZERO;
        
        return baseShipping.add(weightSurcharge).add(expressSurcharge);
    }
    
    /**
     * 私有方法6: 计算重量附加费
     */
    private BigDecimal calculateWeightSurcharge(double totalWeight) {
        if (totalWeight <= 5.0) {
            return BigDecimal.ZERO;
        } else if (totalWeight <= 10.0) {
            return new BigDecimal("5.00");
        } else if (totalWeight <= 20.0) {
            return new BigDecimal("10.00");
        } else {
            return new BigDecimal("20.00");
        }
    }
    
    /**
     * 公共方法:仅计算折扣(暴露部分私有逻辑用于测试)
     */
    public BigDecimal calculateDiscountOnly(BigDecimal subtotal, String promoCode, Customer customer) {
        return calculateDiscount(subtotal, promoCode, customer);
    }
}

/**
 * 通过公共方法测试私有方法的综合性测试
 */
@ExtendWith(MockitoExtension.class)
class OrderPricingServiceTest {

    @Mock
    private TaxCalculator taxCalculator;

    @Mock
    private DiscountService discountService;

    @InjectMocks
    private OrderPricingService pricingService;

    private Order testOrder;
    private Customer regularCustomer;
    private Customer vipCustomer;

    @BeforeEach
    void setUp() {
        regularCustomer = Customer.builder()
                .id(1L)
                .name("普通客户")
                .vip(false)
                .build();
                
        vipCustomer = Customer.builder()
                .id(2L)
                .name("VIP客户")
                .vip(true)
                .build();

        testOrder = Order.builder()
                .id(1001L)
                .customer(regularCustomer)
                .items(Arrays.asList(
                        OrderItem.builder()
                                .productId(1L)
                                .productName("商品A")
                                .unitPrice(new BigDecimal("100.00"))
                                .quantity(2)
                                .build(),
                        OrderItem.builder()
                                .productId(2L)
                                .productName("商品B")
                                .unitPrice(new BigDecimal("50.00"))
                                .quantity(3)
                                .build()
                ))
                .totalWeight(3.0)
                .expressShipping(false)
                .shippingAddress(Address.builder().city("北京").build())
                .build();
    }

    /**
     * 测试私有验证逻辑 - 通过公共方法的异常情况
     */
    @Test
    @DisplayName("计算价格 - 空订单时应抛出异常(测试私有验证逻辑)")
    void calculateTotalPrice_WithNullOrder_ShouldThrowException() {
        assertThatThrownBy(() -> pricingService.calculateTotalPrice(null, null))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("订单不能为空");
    }

    @Test
    @DisplayName("计算价格 - 空商品列表时应抛出异常(测试私有验证逻辑)")
    void calculateTotalPrice_WithEmptyItems_ShouldThrowException() {
        Order emptyOrder = Order.builder()
                .id(1002L)
                .customer(regularCustomer)
                .items(Collections.emptyList())
                .build();

        assertThatThrownBy(() -> pricingService.calculateTotalPrice(emptyOrder, null))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("订单商品不能为空");
    }

    /**
     * 测试私有计算逻辑 - 通过最终结果验证
     */
    @Test
    @DisplayName("计算价格 - 正常订单应正确计算所有分量")
    void calculateTotalPrice_WithNormalOrder_ShouldCalculateCorrectly() {
        // Given
        given(discountService.getDiscountRate(anyString())).willReturn(BigDecimal.ZERO);
        given(taxCalculator.calculateTax(any(BigDecimal.class), any(Address.class)))
                .willReturn(new BigDecimal("35.00"));

        // When
        OrderPrice result = pricingService.calculateTotalPrice(testOrder, null);

        // Then - 验证各个私有方法的计算结果
        // 商品小计: 100*2 + 50*3 = 200 + 150 = 350
        assertThat(result.getSubtotal()).isEqualByComparingTo("350.00");
        
        // 折扣金额: 普通客户无折扣 = 0
        assertThat(result.getDiscountAmount()).isEqualByComparingTo("0.00");
        
        // 折扣后金额: 350 - 0 = 350
        assertThat(result.getAmountAfterDiscount()).isEqualByComparingTo("350.00");
        
        // 税费: Mock返回35
        assertThat(result.getTaxAmount()).isEqualByComparingTo("35.00");
        
        // 运费: 金额超过200免运费 = 0
        assertThat(result.getShippingCost()).isEqualByComparingTo("0.00");
        
        // 总价: 350 + 35 + 0 = 385
        assertThat(result.getTotalAmount()).isEqualByComparingTo("385.00");
    }

    /**
     * 通过公共方法测试私有折扣计算逻辑
     */
    @Test
    @DisplayName("计算折扣 - VIP客户应享受会员折扣")
    void calculateDiscountOnly_ForVIPCustomer_ShouldIncludeVIPDiscount() {
        // When
        BigDecimal discount = pricingService.calculateDiscountOnly(
                new BigDecimal("1000.00"), null, vipCustomer);

        // Then - VIP客户有5%折扣: 1000 * 5% = 50
        assertThat(discount).isEqualByComparingTo("50.00");
    }

    @Test
    @DisplayName("计算折扣 - 大额订单应享受批量折扣")
    void calculateDiscountOnly_ForLargeOrder_ShouldIncludeBulkDiscount() {
        // When - 普通客户,大额订单(1001元)
        BigDecimal discount = pricingService.calculateDiscountOnly(
                new BigDecimal("1001.00"), null, regularCustomer);

        // Then - 大额订单3%折扣: 1001 * 3% ≈ 30.03
        assertThat(discount).isEqualByComparingTo("30.03");
    }

    @Test
    @DisplayName("计算折扣 - 组合折扣应正确计算且不超过上限")
    void calculateDiscountOnly_WithMultipleDiscounts_ShouldCapAtMaximum() {
        // Given
        given(discountService.getDiscountRate("PROMO20")).willReturn(new BigDecimal("0.20"));

        // When - VIP客户(5%) + 促销码(20%) + 大额订单(3%) = 28%,但上限30%
        BigDecimal discount = pricingService.calculateDiscountOnly(
                new BigDecimal("1001.00"), "PROMO20", vipCustomer);

        // Then - 总折扣率28%: 1001 * 28% ≈ 280.28
        assertThat(discount).isEqualByComparingTo("280.28");
    }

    /**
     * 参数化测试运费计算逻辑
     */
    @ParameterizedTest
    @DisplayName("计算价格 - 不同重量和金额的运费计算")
    @MethodSource("provideShippingTestCases")
    void calculateTotalPrice_WithVariousWeightsAndAmounts_ShouldCalculateShippingCorrectly(
            double weight, BigDecimal amount, boolean express, BigDecimal expectedShipping) {
        
        // Given
        Order order = Order.builder()
                .id(1003L)
                .customer(regularCustomer)
                .items(Arrays.asList(
                        OrderItem.builder()
                                .productId(1L)
                                .unitPrice(amount)
                                .quantity(1)
                                .build()
                ))
                .totalWeight(weight)
                .expressShipping(express)
                .shippingAddress(Address.builder().city("上海").build())
                .build();

        given(discountService.getDiscountRate(anyString())).willReturn(BigDecimal.ZERO);
        given(taxCalculator.calculateTax(any(BigDecimal.class), any(Address.class)))
                .willReturn(BigDecimal.ZERO);

        // When
        OrderPrice result = pricingService.calculateTotalPrice(order, null);

        // Then
        assertThat(result.getShippingCost()).isEqualByComparingTo(expectedShipping);
    }

    private static Stream<Arguments> provideShippingTestCases() {
        return Stream.of(
                // 重量 <= 5kg, 金额 <= 200: 基础运费15
                Arguments.of(3.0, new BigDecimal("150.00"), false, new BigDecimal("15.00")),
                // 重量 <= 5kg, 金额 > 200: 免运费
                Arguments.of(3.0, new BigDecimal("250.00"), false, BigDecimal.ZERO),
                // 重量 8kg, 金额 <= 200: 基础15 + 重量附加5 = 20
                Arguments.of(8.0, new BigDecimal("150.00"), false, new BigDecimal("20.00")),
                // 重量 15kg, 金额 <= 200: 基础15 + 重量附加10 = 25  
                Arguments.of(15.0, new BigDecimal("150.00"), false, new BigDecimal("25.00")),
                // 紧急配送: 基础15 + 紧急附加10 = 25
                Arguments.of(3.0, new BigDecimal("150.00"), true, new BigDecimal("25.00")),
                // 重量15kg + 紧急配送: 基础15 + 重量附加10 + 紧急附加10 = 35
                Arguments.of(15.0, new BigDecimal("150.00"), true, new BigDecimal("35.00"))
        );
    }
}

4. 关键总结

私有方法测试的最佳实践

  1. 首选策略:通过公共接口测试私有方法

    • 设计测试用例覆盖私有方法的各种分支
    • 通过最终结果验证私有方法的正确性
  2. 重构策略:提取复杂私有逻辑到单独的类

    • 将复杂的私有方法变为公共方法
    • 提高代码的可测试性和可维护性
  3. 避免策略:不要使用反射测试私有方法

    • 破坏封装性
    • 测试变得脆弱
    • 维护成本高
  4. 设计原则:编写可测试的代码

    • 单一职责原则
    • 依赖注入
    • 面向接口编程

记住:如果你觉得需要测试私有方法,这通常是一个设计信号,表明你的类可能做了太多事情,需要考虑重构。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值