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

对私有方法进行单元测试 NULL 博文链接:https://samjavaeye.iteye.com/blog/2026212 立即下载

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

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. 设计原则:编写可测试的代码

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

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

三菱 FX5U PLC 4轴程序 控制松下伺服3个, 步进电机一个 三菱 FX5U PLC 4轴程序。 控制松下伺服3个, 步进电机一个, 四轴自动堆垛码垛设备程序, 回原点动作用专用的原点回归指令写的, 手动运行用三菱相对定位指令写的 , 自动运行用绝对定位指令写的取料运行, 表格定位写的自动放料运行, 设备各种报警异常都有写出 程序每一步都有注释。 包括PLC程序、显控触摸屏 程序、松下伺服驱动器接线图。 程序内都有IO注释 编号:4816635519484681汇美0808 ... 阅读详情

相关推荐

告别CANoe?手把手教你用Python+PCAN搭建汽车诊断脚本(附完整代码)

本文详细介绍了如何利用Python和PCAN硬件搭建高性价比的汽车诊断自动化方案,替代昂贵的CANoe工具。通过开源库python-can和udsoncan实现UDS诊断协议,涵盖硬件选择、环境配置、核心功能实现及性能优化,附完整代码示例,助力开发者快速构建诊断脚本。

weixin_42551227的博客 373

Java单元测试对私有方法测试

本文简要讲了为什么测试私有方法不是一个好的实践,其次演示了如果一定要测试私有方法,如何通过反射的方式进行测试。

Revivedsun的专栏 5599

ollama接入图像识别大模型

ollama接入图像识别大模型

qq_36437991的博客 2075

替换手机号中间4位(php,js)

JS方法function hideMobile(num){ var mphone =num.substr(3,4); var lphone = num.replace(mphone,"****"); return lphone; } php方法 $phone=substr_replace($phone,'****',3,4);

mobeinight的专栏 2030

学Simulink--基于多能源系统协同与能源互联网的电网系统场景实例:风光氢储一体化系统的能量管理与调度仿真

在全球“双碳”目标驱动下,高比例可再生能源并网成为电网发展的核心方向。然而,风电与光伏具有强随机性与间歇性,给电网稳定运行带来巨大挑战。风光氢储一体化系统(Wind-Solar-Hydrogen-Storage Integrated System)通过将可再生能源发电电解水制氢氢气储存与燃料电池发电有机结合,构建了一个长时储能与能量形态转换的闭环系统,是实现能源互联网与零碳电力系统的关键路径。✅平抑波动:氢储能可实现跨天、跨周储能✅能量转化:电→氢→电/热/交通燃料,实现多能互补✅长时储能。

amy_mhd的博客 1069

ArcGIS基础:CAD数据转换为GIS数据(符号化显示、数据转换、线转面、空间连接)

1、符号化显示: CAD数据加载进来有很多列表,包含了点【point】、线【polyline】、面【polygon】、标注数据【Annotation】等数据,本次实验我们需要用到的是面数据,也就是.dwgPolygong格式的数据。如下图所示: 把数据加载进来,然后在2处打开属性表,查看CAD数据的属性表,打开后可以发现【layer】图层表示就是地块的用地性质,它就是符号化显示的依据。 我们在数据上右键【属性】,打开图层属性,找到【符号系统】,选择类别中的【唯一值】,然后在【值字段】下拉选择【layer】,

yuanYW7556的博客 2万+

JAVA单元测试怎么测试私有方法

如果你希望写个单元测试去测试你写的私有方法,直接使用类实例去调用私有方法是不行的,这里推荐使用反射来访问和调用私有方法。注意:示例中的测试方法注意要抛异常。

sunglow1126的博客 1470

基于matlab的CNN物体识别物体分类系统

主要对物体进行识别,搭建一个物体分类系统本次实验选择规模稍大的 CIFAR-10 数据集进行分类识别,对应图像的大小设计 VggNet、ResNet 进行网络训练。在实际操作过程中,受限于数据规模和硬件配置,对于大小放缩、批次参数、训练步数等进行了多次调试,这也对以后的图像分类任务具有较高的参考意义,并通过对 CIFAR-10 数据集进行训练和评测,采用网络修改的方式来得到新的 CNN 结构,适用于对网络进行配置、融合等相关研究。读者可以通过自行设计或选择其他网络、加载其他数据等方式进行实验的延伸。

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 2005

junit单元测试mock私有private方法和静态static方法

org.powermock.api.mockito.PowerMockito更强大,支持private和protected方法的mock。org.mockito.Mockito功能有限,不能mock 私有private、受保护的protected方法。利用PowerMockito来mock私有方法需要mock。

星空的风fly 2万+

GNN(图神经网络)的 MATLAB 仿真

图神经网络(Graph Neural Network, GNN)是一种用于处理图结构数据的深度学习模型。GNN 通过聚合节点及其邻居的信息,能够有效捕捉图结构中的复杂关系,广泛应用于社交网络分析、推荐系统、化学分子分析等领域。GNN 通过消息传递机制聚合节点及其邻居的信息,能够有效处理图结构数据。MATLAB 仿真展示了 GNN 的基本流程和节点分类性能,适用于社交网络分析、推荐系统、化学分子分析和知识图谱等多种应用场景。

走向CTO的路上... 836

openlayers6/7 行政区划裁剪 只显示某个区域 示例为天地图

openlayers6/7 行政区划裁剪 只显示某个区域 示例为天地图。

qq_36287830的博客 3100

Spring单元测试001-如何测试私有方法

Spring单元测试私有方法 @RunWith(SpringRunner.class) @SpringBootTest(classes = ServerApplication.class) public class XXXTest extends XXXServiceImpl { /** * 测试私有方法示例及说明: * 想要测试某类中的私有方法,必须通过反射。 * 反射获取私有方法有两种方式,一种是直接new对象,然后通过getClass;一种是extends要测试的

u014755679的博客 4366

基于STM32F4XX 、HAL库的GXHT30C温湿度传感器驱动应用C语言程序设计

define GXHT30C_STATUS_WRITE_CHK (1 << 0) // 写数据校验状态。#define GXHT30C_STATUS_HUMI_TRACK (1 << 11) // 湿度跟踪报警。#define GXHT30C_STATUS_TEMP_TRACK (1 << 10) // 温度跟踪报警。#define GXHT30C_STATUS_CMD_STATUS (1 << 1) // 命令执行状态。

PP_Cottage的博客 226

private(私有)方法单元测试无法覆盖?那就用反射调用来测试private(私有)方法

代码测试覆盖率是衡量软件质量的重要指标,但常规的Junit不能对private方法进行测试,那么就会影响我们的覆盖率。 不过有一个办法可以在Junit框架中测试private的方法,那就是反射! 我们知道可以通过反射method.invoke来调用某个方法,但如果方法前面为private怎么办呢?我们可以使用method.setAccessible(true);来跳过安全检查,就可以调用pri

上海男孩博客 2463

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

java私有方法单元测试_Spring-boot单元测试(私有方法测试)

Spring-boot的单元测试网上有了很多,当项目是可以使用spring-boot正常运行时,只要在测试类上添加如下配置就使用@Autowired的方式进行单元测试@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = Application.class)现在的场景时,我要测试的时这个类中的某个私有方法的功能,代码如下@R...

weixin_32643811的博客 1048

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

如何对私有方法进行单元测试?(依据推荐等级排序)

<br /><br />如何对私有方法进行单元测试?(依据推荐等级排序)<br />1.         不测试,没必要测试private方法。(但是1.c需要保证,或者参考TDD)<br />a)         私有的方法一定是供暴露出来的方法调用的,测试了暴露方法,也就同时测试了私有方法,如果做不到,是否代码重构有问题?<br />b)         单元测试的目的是为了保证你修改复用代码时不会影响到所有引用这段代码的程序,private方法本来就不能被别的类引用,所以不需要用单元测试来保证其

sharpljc的专栏 2235

java私有方法单元测试_如何通过java反射的方式对java私有方法进行单元测试

待测试的私有方法:import org.testng.Assert;import org.testng.annotations.BeforeClass;import org.testng.annotations.Test;import springfox.documentation.service.ApiInfo;import java.lang.reflect.Method;public cla...

weixin_39997300的博客 296

使用Sinon和Rewire对JavaScript中的私有方法进行单元测试

我们曾经试图遵循良好的编程习惯,在创建和定义方法时尽可能按照“职责单一”和“开放-封闭”原则将那些没有必要暴露出来的方法定义为私有方法,但是在编写测试用例时又往往对这些设计原则嗤之以鼻,因为你会为无法编写测试这些私有方法的测试用例而感到苦恼。

自动化测试技术栈 662

private(私有)方法单元测试无法覆盖?那就用反射调用来测试private(私有)方法...

请移步

时间的灰烬 812

1 私有方法单元测试 ;2 反射调用方法;单元测试中 私有成员变量赋值;

私有方法单元测试、及单元测试中 私有成员变量赋值

LMY18238816299的博客 1620

java私有方法单元测试_java – 使用包私有方法以便于单元测试是一个好习惯吗?...

有时我发现自己处于这样的情况,如果我将某些方法的可见性从私有更改为私有,以便更容易进行单元测试模拟,断言……一个例子就是这样假设我有一个包含4个属性X,Y,Z和R的对象A,其中X,Y和Z是集合,R是每个集合的不同元素之间的关系,例如,关系将由X的元素组成, Y的元素和Z的元素.对象A不允许直接访问X,Y,Z或R,而是提供了一个丰富的API,允许您在X,Y和Z上创建新元素,也允许您将这些元素混合到新...

weixin_39760857的博客 269

Java+SpringBoot单元测试,利用反射测试私有方法

#在sringBoot方法中测试私有方法需要利用反射 import com.sun.tools.javah.Gen; import java.io.BufferedOutputStream; import java.lang.reflect.Method; class Game { Game(int i) { System.out.print("Game construc...

m0_37744119的博客 2304

Java对私有方法进行单元测试及示例

在上述示例中,我们创建了一个MyClass类,其中有一个私有方法privateMethod。首先,我们使用getDeclaredMethod方法获取私有方法的引用,并使用setAccessible方法将其设置为可访问。然后,我们使用invoke方法调用私有方法,并传递相应的参数。然而,如果有必要测试私有方法,可以使用反射来访问和调用私有方法。需要注意的是,测试私有方法可能会违反封装原则,因为我们直接访问了类的内部实现。因此,在进行单元测试时,应优先测试公共接口,而不是直接测试私有方法。

m0_46305352的博客 3305

Java私有方法的单元测试

可以利用 Java 的反射机制来访问和调用私有方法。将私有方法的逻辑封装到公有方法中。

m0_74775308的博客 748

首页文章分类 关于 Search private(私有)方法单元测试无法覆盖?那就用反射调用来测试private(私有)方法

import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class PrivateMethodTestUtils { public static Object invoke(Object methodHostInstance, String methodN

wuxianpishuang的专栏 443

PowerMockito单元测试(3)-私有静态方法单元测试与stub方法

3. 私有静态方法单元测试:  【公共方法&私有方法】: public class UserUtil{ public static String generateUserToken(String userId){ if(StringUtils.isEmpty(userId)){ return StringUtils.EMPTY; } PublicKey key = getPublicKey(userId); if(key == null){ return Strin

假人一个的博客 4924
上一篇: 在单元测试中, Mockito 框架中 when() 和 given() 两者的作用和区别
下一篇: PostgreSQL 字段类型选择最佳实践 - Java 开发者指南
龙茶清欢
博客等级 码龄7年 3720粉丝 375原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

龙茶清欢

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

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

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

打赏作者

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

抵扣说明:

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

余额充值