私有方法单元测试全面指南
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. 关键总结
私有方法测试的最佳实践
-
首选策略:通过公共接口测试私有方法
- 设计测试用例覆盖私有方法的各种分支
- 通过最终结果验证私有方法的正确性
-
重构策略:提取复杂私有逻辑到单独的类
- 将复杂的私有方法变为公共方法
- 提高代码的可测试性和可维护性
-
避免策略:不要使用反射测试私有方法
- 破坏封装性
- 测试变得脆弱
- 维护成本高
-
设计原则:编写可测试的代码
- 单一职责原则
- 依赖注入
- 面向接口编程
记住:如果你觉得需要测试私有方法,这通常是一个设计信号,表明你的类可能做了太多事情,需要考虑重构。
1133

被折叠的 条评论
为什么被折叠?



