← Назад к вопросам

Что такое Сode Сoverage?

1.7 Middle🔥 161 комментариев
#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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% покрытия любой ценой.