Статьи

Ваш показатель покрытия кода не имеет смысла

На прошлой неделе у меня были жаркие, но интересные  дебаты в Twitter  о Code Coverage с моим давним другом (а иногда и партнером по сквошу) Фредди Маллетом.

Суть моей точки зрения заключается в следующем: показатель покрытия кода, который лелеют большинство разработчиков программного обеспечения, ничего не гарантирует. Таким образом, достижение охвата кода 80% (или 100%) и хвастовство так же полезно, как и ветер. Конечно, довольно сложно вести дискуссию на основе фактов через Twitter, поскольку 140 символов накладывают жесткие ограничения на любые аргументы. Эта статья — попытка записать мои аргументы в безграничном пространстве.

Бесполезность исходного кода покрытия может быть доказана довольно легко. Давайте рассмотрим простой пример со следующим классом для проверки:

public class PassFilter {
 
    private int limit;
 
    public PassFilter(int limit) {
        this.limit = limit;
    }
 
    public boolean filter(int i) {
        return i < limit;
    }
}

Этот класс довольно прост, нет необходимости комментировать. Возможный тест будет следующим:

public class PassFilterTest {
 
    private PassFilter passFilterFive;
 
    @BeforeMethod
    protected void setUp() {
        passFilterFive = new PassFilter(5);
    }
 
    @Test
    public void should_pass_when_filtering_one() {
        boolean result = passFilterFive.filter(1);
    }
 
    @Test
    public void should_not_pass_when_filtering_ten() {
        boolean result = passFilterFive.filter(10);
    }
}

Этот тестовый класс с радостью вернет 100% -ное покрытие кода, а также 100% -ное покрытие строк: выполнение теста пройдет через все строки кода и по обе стороны от его отдельной ветви. Разве жизнь не сладка? Жаль, что нет никаких утверждений; они могли быть нарочно «забыты» подрядчиком, который не смог достичь ранее согласованного показателя покрытия кода. Давайте дадим подрядчику выгоду от сомнений и предположим, что программисты добросовестны, и сделаем утверждения:

public class PassFilterTest {
 
    private PassFilter passFilterFive;
 
    @BeforeMethod
    protected void setUp() {
        passFilterFive = new PassFilter(5);
    }
 
    @Test
    public void should_pass_when_filtering_one() {
        boolean result = passFilterFive.filter(1);
        Assert.assertTrue(result);
    }
 
    @Test
    public void should_not_pass_when_filtering_ten() {
        boolean result = passFilterFive.filter(10);
        Assert.assertFalse(result);
    }
}

Все еще 100% покрытие кода и 100% покрытие линии — и на этот раз «реальные» утверждения! Но это все еще бесполезно … Это общеизвестный факт, что разработчики, как правило, тестируют для прохождения кейсов. В этом случае в двух случаях используются параметры 1 и 10, в то время как потенциальная ошибка находится на точном пороге 5 (должен ли фильтр пропустить это значение или нет с этим значением?).

В заключение, необработанное покрытие кода только гарантирует  максимально возможное  покрытие кода. Если это 0%, конечно, это будет 0%; однако, с 80%, это не дает вам ничего … только страховка, что в большинстве случаев  ваш код покрывается на 80%. Но это также может быть что-то среднее, 60%, 40% … или даже 0%. Что хорошего в метрике, которая только намекает на максимум? На самом деле, в этом свете покрытие кода — это  фильтр Блума . ИМХО, единственный способ гарантировать, что тестовые случаи и соответствующее тестовое покрытие действительно значимы, — это использовать  Mutation Testing . Для ознакомления с базовым введением в Mutation Testing , пожалуйста, ознакомьтесь с моим   докладом Введение в Mutation Testing на JavaZone (10 минут).

Хорошая вещь заключается в том, что Mutation Testing — это не академическая статья, известная только ботаникам, есть инструменты на Java, чтобы начать использовать ее прямо сейчас. Настройка  PIT  с помощью предыдущего теста даст следующий результат:

В этом отчете указывается оставшийся мутант для уничтожения и связанная с ним строка (строка 12), и легко добавить отсутствующий тестовый пример:

@Test
public void should_not_pass_when_filtering_five() {
    boolean result = passFilterFive.filter(5);
    Assert.assertFalse(result);
}

Теперь, когда мы без сомнения продемонстрировали ценность Mutation Testing, что мы делаем? Некоторые могут попробовать пару аргументов против мутационного тестирования. Давайте рассмотрим каждый из них:

  • Тестирование на мутацию занимает много времени. Конечно, комбинация всех возможных мутаций занимает гораздо больше времени, чем стандартное модульное тестирование, которое должно быть менее 10 минут. Однако это не является проблемой, поскольку охват кода не является метрикой, которую необходимо проверять при каждой сборке: ночной сборки более чем достаточно.
  • Тестирование мутаций занимает много времени для анализа результатов. Точно так же … но в какое время анализировать результаты покрытия кода, которые, как было показано ранее, только намекают на максимально возможное покрытие кода?

С одной стороны, необработанная метрика покрытия кода актуальна только в том случае, если она слишком низкая, а при высоком уровне требует дальнейшего анализа. С другой стороны, Mutation Testing позволяет вам доверять метрике Code Coverage  без дополнительных затрат . Остальное зависит от тебя…