Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Code Coverage
Code Coverage — это метрика, которая измеряет процент исходного кода, который покрыт (выполнен) автоматическими тестами. Показывает, насколько полно программа протестирована, и помогает выявить нетестированные участки кода, которые могут содержать ошибки. Code Coverage обычно выражается в процентах от общего количества строк кода или ветвей выполнения.
Основные метрики Code Coverage
1. Line Coverage (покрытие строк):
Процент строк кода, которые были выполнены хотя бы один раз во время тестирования.
public class Calculator {
public int add(int a, int b) {
return a + b; // Строка 3 - может быть покрыта
}
public int divide(int a, int b) {
if (b == 0) { // Строка 7 - может быть покрыта
throw new IllegalArgumentException("Divisor cannot be zero"); // Строка 8
}
return a / b; // Строка 10
}
}
// Тест покрывает только успешное разделение
@Test
public void testDivide() {
Calculator calc = new Calculator();
assertEquals(5, calc.divide(10, 2)); // Покрывает строки 3, 7, 10
// Строка 8 НЕ покрыта (исключение не выбрасывается)
}
// Покрытие: 75% (3 из 4 строк логики покрыты)
2. Branch Coverage (покрытие ветвей):
Процент условных ветвей (if, else, switch случаев), которые были выполнены.
public class UserValidator {
public boolean isValidUser(User user) {
if (user == null) { // Ветка 1
return false;
}
if (user.getAge() >= 18) { // Ветка 2a (true)
return true;
} else { // Ветка 2b (false)
return false;
}
}
}
// Тест 1: только happy path
@Test
public void testValidUser() {
User user = new User("John", 25);
assertTrue(isValidUser(user)); // Покрывает ветки 2a
// Ветки 1, 2b НЕ покрыты
}
// Тест 2: полное покрытие
@Test
public void testAllBranches() {
assertFalse(isValidUser(null)); // Ветка 1
assertTrue(isValidUser(new User("", 25))); // Ветка 2a
assertFalse(isValidUser(new User("", 17))); // Ветка 2b
}
// Покрытие: 100% (все ветки выполнены)
3. Path Coverage (покрытие путей):
Процент всех возможных путей выполнения кода.
public class OrderProcessor {
public String processOrder(Order order) {
// Путь 1: order == null -> null
if (order == null) {
return "Invalid order";
}
// Путь 2: order.isValid() == false -> Invalid
if (!order.isValid()) {
return "Invalid order";
}
// Путь 3: order.hasPayment() == false -> No payment
if (!order.hasPayment()) {
return "No payment";
}
// Путь 4: Успешно -> Processed
return "Processed";
}
}
// Total paths: 4 (2^2 условия + null проверка)
// Чтобы покрыть все пути нужно 4 теста
4. Method Coverage (покрытие методов):
Процент методов, которые были вызваны минимум один раз.
public class PaymentService {
public void processPayment(Order order) { } // Может быть вызван
public void refundPayment(Order order) { } // Может НЕ быть вызван
public boolean validatePayment(Order order) { } // Может быть вызван
public void logTransaction(Order order) { } // Может НЕ быть вызван
}
// Покрытие методов: 50% (2 из 4 методов покрыты)
Code Coverage в Java с JaCoCo
Maven конфигурация:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<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>PACKAGE</element>
<excludes>
<exclude>*Test</exclude>
</excludes>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.90</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Использование:
mvn test jacoco:report
# Отчёт в target/site/jacoco/index.html
Gradle конфигурация:
plugins {
id 'jacoco'
}
jacoco {
toolVersion = '0.8.8'
}
test {
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true
html.required = true
csv.required = false
}
}
jacocoTestCoverageVerification {
violationRules {
rule {
element = 'PACKAGE'
excludes = ['*Test']
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.90
}
}
}
}
Пример полного покрытия
public class BankAccount {
private double balance;
private int failedAttempts = 0;
private static final int MAX_FAILED_ATTEMPTS = 3;
public BankAccount(double initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException("Balance cannot be negative");
}
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
}
public boolean withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (amount > balance) {
failedAttempts++;
if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
throw new RuntimeException("Account locked");
}
return false;
}
balance -= amount;
failedAttempts = 0;
return true;
}
public double getBalance() {
return balance;
}
}
// Comprehensive test coverage
@ExtendWith(MockitoExtension.class)
public class BankAccountTest {
private BankAccount account;
@BeforeEach
public void setUp() {
account = new BankAccount(1000);
}
// Constructor tests
@Test
public void testConstructorWithValidBalance() {
assertEquals(1000, new BankAccount(1000).getBalance());
}
@Test
public void testConstructorWithNegativeBalanceThrowsException() {
assertThrows(IllegalArgumentException.class, () -> new BankAccount(-100));
}
// Deposit tests
@Test
public void testDepositWithPositiveAmount() {
account.deposit(500);
assertEquals(1500, account.getBalance());
}
@Test
public void testDepositWithZeroThrowsException() {
assertThrows(IllegalArgumentException.class, () -> account.deposit(0));
}
@Test
public void testDepositWithNegativeAmountThrowsException() {
assertThrows(IllegalArgumentException.class, () -> account.deposit(-100));
}
// Withdraw tests
@Test
public void testWithdrawWithSufficientBalance() {
assertTrue(account.withdraw(500));
assertEquals(500, account.getBalance());
}
@Test
public void testWithdrawWithInsufficientBalance() {
assertFalse(account.withdraw(1500));
assertEquals(1000, account.getBalance());
}
@Test
public void testWithdrawWithNegativeAmountThrowsException() {
assertThrows(IllegalArgumentException.class, () -> account.withdraw(-100));
}
@Test
public void testAccountLockedAfterMultipleFailedAttempts() {
assertFalse(account.withdraw(1500));
assertFalse(account.withdraw(1500));
assertThrows(RuntimeException.class, () -> account.withdraw(1500));
}
@Test
public void testFailedAttemptsResetAfterSuccessfulWithdraw() {
account.withdraw(1500); // Fails
account.withdraw(500); // Succeeds, resets counter
account.withdraw(1500); // Fails again
account.withdraw(1500); // Fails again
account.withdraw(1500); // Fails again - should not lock yet (only 3 consecutive)
}
// Coverage: 100% (все строки и ветки)
}
Best Practices для Code Coverage
1. Целевое покрытие - 80-90%, не 100%:
// 100% покрытие может быть достигнуто тестированием тривиального кода
public class Trivial {
public String toString() {
return "Trivial"; // Легко покрыть, но не важно
}
}
// Лучше сосредоточиться на критичном коде
public class PaymentProcessor {
public void processPayment(Payment payment) {
// Критично: должно быть полностью покрыто
}
}
2. Исключай тривиальный код из отчёта:
@Generated("IDE generated code") // Исключается из JaCoCo
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyClass myClass = (MyClass) o;
return id == myClass.id;
}
3. Пиши тесты ДО кода (TDD):
// RED: Тест падает
@Test
public void testUserCanLogin() {
User user = new User("john@example.com", "password123");
assertTrue(user.authenticate("password123"));
}
// GREEN: Написать минимальный код
public class User {
private String password;
public User(String email, String password) {
this.password = password;
}
public boolean authenticate(String password) {
return this.password.equals(password);
}
}
4. Используй параметризованные тесты для многих сценариев:
@ParameterizedTest
@CsvSource({
"10, 20, 30",
"0, 0, 0",
"-10, 10, 0",
"100, 200, 300"
})
public void testAdd(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
5. Интегрируй код-лвраж в CI/CD:
# GitHub Actions example
name: Code Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: '17'
- run: mvn test jacoco:report
- uses: codecov/codecov-action@v2
Инструменты для Code Coverage
Java:
- JaCoCo (рекомендуется) - поддерживает line и branch coverage
- Cobertura - older tool, но всё ещё популярен
- OpenClover - коммерческий инструмент
Сервисы анализа:
- Codecov
- Coveralls
- SonarQube
- Code Climate
Заключение
Code Coverage — это важная метрика для оценки качества тестов, но не единственный показатель. Целевое покрытие 80-90% обычно оптимально для production кода. Сосредоточься на тестировании критических участков (бизнес-логика, обработка ошибок), а не на достижении 100% покрытия любой ценой.