Как тестировать метод с большим количеством ветвлений
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как тестировать метод с большим количеством ветвлений
Методы с множественными условиями (if-else цепочки, switch операторы) требуют особого подхода при тестировании. Необходимо покрыть все возможные пути выполнения для обеспечения полноты тестирования. Рассмотрим стратегии и инструменты.
1. Анализ требований к покрытию
Statement Coverage (Покрытие операторов)
Проверяем, что каждый оператор выполнился хотя бы раз. Минимальный уровень.
Branch Coverage (Покрытие ветвлений)
Проверяем оба пути (true и false) для каждого условия. Этот уровень рекомендуется для большинства приложений.
Path Coverage (Покрытие путей)
Проверяем все возможные комбинации ветвлений. Для N условий это может быть 2^N тестов.
Пример с большим количеством ветвлений
Рассмотрим сложный метод валидации данных для заказа:
public class OrderValidator {
public String validateOrder(Order order) {
// 1. Проверка пустого заказа
if (order == null) {
return "Order is null";
}
// 2. Проверка количества товаров
if (order.getItems() == null || order.getItems().isEmpty()) {
return "Order has no items";
}
// 3. Проверка суммы
BigDecimal total = order.getTotalAmount();
if (total == null || total.compareTo(BigDecimal.ZERO) <= 0) {
return "Invalid order total";
}
// 4. Проверка пользователя
User customer = order.getCustomer();
if (customer == null || customer.getId() == null) {
return "Customer not specified";
}
// 5. Проверка доставки
Address deliveryAddress = order.getDeliveryAddress();
if (deliveryAddress == null) {
return "Delivery address missing";
}
if (!isValidAddress(deliveryAddress)) {
return "Invalid delivery address";
}
// 6. Проверка способа оплаты
PaymentMethod paymentMethod = order.getPaymentMethod();
if (paymentMethod == null) {
return "Payment method not selected";
}
if (paymentMethod == PaymentMethod.CARD && !validateCard(order)) {
return "Invalid card details";
}
if (paymentMethod == PaymentMethod.BANK_TRANSFER && !validateBankAccount(order)) {
return "Invalid bank account";
}
// 7. Проверка скидок
if (order.getDiscount() != null && order.getDiscount().compareTo(total) > 0) {
return "Discount exceeds total";
}
return "OK";
}
private boolean isValidAddress(Address address) {
return address.getCountry() != null &&
address.getCity() != null &&
address.getStreet() != null;
}
private boolean validateCard(Order order) {
// Проверка карты
return order.getCardNumber() != null &&
order.getCardNumber().length() == 16;
}
private boolean validateBankAccount(Order order) {
return order.getBankAccountNumber() != null &&
!order.getBankAccountNumber().isEmpty();
}
}
2. Параметризованные тесты (Parameterized Tests)
Это лучший подход для методов с множественными условиями. Используем JUnit 5 с @ParameterizedTest:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.*;
class OrderValidatorTest {
private OrderValidator validator = new OrderValidator();
// Параметризованный тест с inline данными
@ParameterizedTest(name = "Validate: {0}")
@CsvSource({
"null, Order is null",
"emptyItems, Order has no items",
"zeroAmount, Invalid order total",
"negativeAmount, Invalid order total",
"noCustomer, Customer not specified",
"missingAddress, Delivery address missing",
"invalidAddress, Invalid delivery address",
"noPaymentMethod, Payment method not selected",
"invalidCard, Invalid card details",
"invalidBankAccount, Invalid bank account",
"discountExceedsTotal, Discount exceeds total",
"valid, OK"
})
void testOrderValidation(String scenario, String expectedResult) {
Order order = createOrderByScenario(scenario);
String result = validator.validateOrder(order);
assertEquals(expectedResult, result);
}
// Параметризованный тест с методом источника
@ParameterizedTest
@MethodSource("provideOrderTestCases")
void testOrderValidationAdvanced(Order order, String expected) {
String result = validator.validateOrder(order);
assertEquals(expected, result);
}
static java.util.stream.Stream<org.junit.jupiter.params.provider.Arguments>
provideOrderTestCases() {
return java.util.stream.Stream.of(
org.junit.jupiter.params.provider.Arguments.of(
null,
"Order is null"
),
org.junit.jupiter.params.provider.Arguments.of(
Order.builder().items(List.of()).build(),
"Order has no items"
),
org.junit.jupiter.params.provider.Arguments.of(
Order.builder()
.items(List.of(new Item()))
.totalAmount(BigDecimal.ZERO)
.build(),
"Invalid order total"
),
org.junit.jupiter.params.provider.Arguments.of(
createValidOrder(),
"OK"
)
);
}
private Order createOrderByScenario(String scenario) {
return switch (scenario) {
case "null" -> null;
case "emptyItems" -> Order.builder()
.items(List.of())
.build();
case "zeroAmount" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(BigDecimal.ZERO)
.build();
case "negativeAmount" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("-10"))
.build();
case "noCustomer" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("100"))
.customer(null)
.build();
case "missingAddress" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("100"))
.customer(new User())
.deliveryAddress(null)
.build();
case "invalidAddress" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("100"))
.customer(new User())
.deliveryAddress(new Address()) // Пустой адрес
.build();
case "noPaymentMethod" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("100"))
.customer(new User())
.deliveryAddress(Address.builder()
.country("RU")
.city("Moscow")
.street("Main St")
.build())
.paymentMethod(null)
.build();
case "invalidCard" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("100"))
.customer(new User())
.deliveryAddress(validAddress())
.paymentMethod(PaymentMethod.CARD)
.cardNumber("123") // Неправильная длина
.build();
case "invalidBankAccount" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("100"))
.customer(new User())
.deliveryAddress(validAddress())
.paymentMethod(PaymentMethod.BANK_TRANSFER)
.bankAccountNumber("")
.build();
case "discountExceedsTotal" -> Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("100"))
.customer(new User())
.deliveryAddress(validAddress())
.paymentMethod(PaymentMethod.CARD)
.cardNumber("1234567890123456")
.discount(new BigDecimal("150"))
.build();
case "valid" -> createValidOrder();
default -> throw new IllegalArgumentException("Unknown scenario: " + scenario);
};
}
private Order createValidOrder() {
return Order.builder()
.items(List.of(new Item()))
.totalAmount(new BigDecimal("100"))
.customer(new User())
.deliveryAddress(validAddress())
.paymentMethod(PaymentMethod.CARD)
.cardNumber("1234567890123456")
.discount(new BigDecimal("10"))
.build();
}
private Address validAddress() {
return Address.builder()
.country("RU")
.city("Moscow")
.street("Main St")
.build();
}
}
3. Decision Table Testing (Таблица решений)
Для очень сложных логик используй табличный подход:
class PaymentProcessorTest {
// Таблица: Тип платежа × Сумма × Статус аккаунта = Ожидаемый результат
record PaymentTestCase(
PaymentMethod method,
BigDecimal amount,
AccountStatus accountStatus,
boolean expectedSuccess,
String expectedErrorCode
) {}
static List<PaymentTestCase> paymentTestCases() {
return List.of(
// method amount status success error
new PaymentTestCase(
PaymentMethod.CARD,
new BigDecimal("100"),
AccountStatus.ACTIVE,
true,
null
),
new PaymentTestCase(
PaymentMethod.CARD,
new BigDecimal("100000"),
AccountStatus.ACTIVE,
false,
"LIMIT_EXCEEDED"
),
new PaymentTestCase(
PaymentMethod.BANK_TRANSFER,
new BigDecimal("100"),
AccountStatus.SUSPENDED,
false,
"ACCOUNT_SUSPENDED"
)
);
}
@ParameterizedTest
@MethodSource("paymentTestCases")
void testPaymentProcessing(PaymentTestCase testCase) {
PaymentProcessor processor = new PaymentProcessor();
PaymentResult result = processor.process(
testCase.method,
testCase.amount,
testCase.accountStatus
);
assertEquals(testCase.expectedSuccess, result.isSuccess());
if (!testCase.expectedSuccess) {
assertEquals(testCase.expectedErrorCode, result.getErrorCode());
}
}
}
4. Проверка покрытия с JaCoCo
Убедись, что все ветви покрыты:
<!-- pom.xml -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>CLASS</element>
<excludes>
<exclude>*Test</exclude>
</excludes>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.75</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
5. Рефакторинг сложного кода
Если метод имеет слишком много ветвлений, рассмотри рефакторинг:
Strategy Pattern
interface OrderValidationStrategy {
String validate(Order order);
}
class CardPaymentValidation implements OrderValidationStrategy {
@Override
public String validate(Order order) {
return order.getCardNumber() != null && order.getCardNumber().length() == 16
? "OK"
: "Invalid card";
}
}
class PaymentMethodValidator {
private Map<PaymentMethod, OrderValidationStrategy> validators;
public String validate(Order order) {
OrderValidationStrategy validator =
validators.get(order.getPaymentMethod());
return validator != null ? validator.validate(order) : "Unknown method";
}
}
Guard Clauses
// До
if (condition1) {
if (condition2) {
if (condition3) {
// основная логика
}
}
}
// После
if (!condition1) return error1;
if (!condition2) return error2;
if (!condition3) return error3;
// основная логика
Лучшие практики
- Используй параметризованные тесты для комбинационного тестирования
- Цель: Branch Coverage >= 80% (все else ветви)
- Разбивай сложные методы на несколько простых
- Используй таблицы решений для сложной бизнес-логики
- Проверяй граничные значения (null, пустые коллекции, 0, отрицательные)
- Используй Builder pattern для создания тестовых данных
- Мокируй зависимости для изоляции тестируемого метода