Перейти к основному содержимому

Мутационное тестирование с помощью PITest

· 6 мин. чтения

1. Обзор

Тестирование программного обеспечения относится к методам, используемым для оценки функциональности программного приложения. В этой статье мы собираемся обсудить некоторые метрики, используемые в индустрии тестирования программного обеспечения, такие как покрытие кода и мутационное тестирование , с особым интересом к тому, как выполнить мутационное тестирование с помощью библиотеки PITest .

Для простоты мы собираемся основывать эту демонстрацию на базовой функции палиндрома. Обратите внимание, что палиндром — это строка, которая читается одинаково вперед и назад.

2. Зависимости Maven

Как видно из конфигурации зависимостей Maven, мы будем использовать JUnit для запуска наших тестов и библиотеку PITest для внедрения мутантов в наш код — не волнуйтесь, через секунду мы увидим, что такое мутант. Вы всегда можете найти последнюю версию зависимостей в центральном репозитории maven, перейдя по этой ссылке .

<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-parent</artifactId>
<version>1.1.10</version>
<type>pom</type>
</dependency>

Чтобы библиотека PITest работала, нам также необходимо включить плагин pitest-maven в наш файл конфигурации pom.xml :

<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.1.10</version>
<configuration>
<targetClasses>
<param>com.foreach.testing.mutation.*</param>
</targetClasses>
<targetTests>
<param>com.foreach.mutation.test.*</param>
</targetTests>
</configuration>
</plugin>

3. Настройка проекта

Теперь, когда у нас настроены зависимости Maven, давайте посмотрим на эту не требующую пояснений функцию-палиндром:

public boolean isPalindrome(String inputString) {
if (inputString.length() == 0) {
return true;
} else {
char firstChar = inputString.charAt(0);
char lastChar = inputString.charAt(inputString.length() - 1);
String mid = inputString.substring(1, inputString.length() - 1);
return (firstChar == lastChar) && isPalindrome(mid);
}
}

Все, что нам нужно сейчас, это простой тест JUnit, чтобы убедиться, что наша реализация работает так, как нужно:

@Test
public void whenPalindrom_thenAccept() {
Palindrome palindromeTester = new Palindrome();
assertTrue(palindromeTester.isPalindrome("noon"));
}

Пока все хорошо, мы готовы успешно запустить наш тестовый пример в качестве теста JUnit.

Далее в этой статье мы сосредоточимся на покрытии кода и мутаций с помощью библиотеки PITest.

4. Покрытие кода

Покрытие кода широко используется в индустрии программного обеспечения для измерения того, какой процент путей выполнения был использован во время автоматизированных тестов.

Мы можем измерить эффективное покрытие кода на основе путей выполнения, используя такие инструменты, как Eclemma, доступные в Eclipse IDE.

После запуска TestPalindrome с покрытием кода мы можем легко достичь 100%-го показателя покрытия. Обратите внимание, что isPalindrome является рекурсивным, поэтому совершенно очевидно, что проверка длины пустого ввода все равно будет покрыта.

К сожалению, метрики покрытия кода иногда могут быть совершенно неэффективными , потому что показатель 100% покрытия кода означает только то, что все строки были проверены хотя бы один раз, но ничего не говорит о точности тестов или полноте вариантов использования , и поэтому мутационное тестирование действительно имеет значение.

5. Покрытие мутаций

Мутационное тестирование — это метод тестирования, используемый для повышения адекватности тестов и выявления дефектов в коде. Идея состоит в том, чтобы динамически изменять производственный код и вызывать сбои тестов.

Хорошие тесты провалятся

Каждое изменение в коде называется мутацией , и в результате получается измененная версия программы, называемая мутацией .

Мы говорим, что мутация уничтожена , если она может вызвать сбой в тестах. Мы также говорим, что мутация сохранилась , если мутант не мог повлиять на поведение тестов.

Теперь давайте запустим тест с помощью Maven, установив для параметра target значение: org.pitest:pitest-maven:mutationCoverage .

Мы можем проверить отчеты в формате HTML в каталоге target/pit-test/YYYYMMDDHHMI :

  • 100% покрытие линии: 7/7
  • 63% охват мутаций: 5/8

Очевидно, что наш тест проходит по всем путям выполнения, поэтому показатель покрытия строк составляет 100%. С другой стороны, библиотека PITest представила 8 мутантов , 5 из них были убиты — вызвали сбой — но 3 выжили.

Мы можем проверить отчет com.foreach.testing.mutation/Palindrome.java.html для получения более подробной информации о созданных мутантах:

./70a78d63699b33958c62fcac65950783.png



Это мутаторы, активные по умолчанию при запуске теста покрытия мутаций:

  • INCREMENTS_MUTATOR
  • VOID_METHOD_CALL_MUTATOR
  • RETURN_VALS_MUTATOR
  • MATH_MUTATOR
  • NEGATE_CONDITIONALS_MUTATOR
  • INVERT_NEGS_MUTATOR
  • CONDITIONALS_BOUNDARY_MUTATOR

Для получения более подробной информации о мутаторах PITest вы можете проверить ссылку на страницу официальной документации .

Наша оценка покрытия мутаций отражает отсутствие тестовых случаев , поскольку мы не можем убедиться, что наша функция палиндрома отклоняет непалиндромные и почти палиндромные входные строки.

6. Улучшить показатель мутации

Теперь, когда мы знаем, что такое мутация, нам нужно улучшить наш показатель мутации, убивая выживших мутантов .

Возьмем в качестве примера первую мутацию — отрицательное условное выражение — в строке 6. Мутант выжил, потому что даже если мы изменим фрагмент кода:

if (inputString.length() == 0) {
return true;
}

К:

if (inputString.length() != 0) {
return true;
}

Испытание пройдет, и поэтому мутация выжила . Идея состоит в том, чтобы реализовать новый тест, который не пройдёт, если будет введен мутант . То же самое можно сделать и с оставшимися мутантами.

@Test
public void whenNotPalindrom_thanReject() {
Palindrome palindromeTester = new Palindrome();
assertFalse(palindromeTester.isPalindrome("box"));
}
@Test
public void whenNearPalindrom_thanReject() {
Palindrome palindromeTester = new Palindrome();
assertFalse(palindromeTester.isPalindrome("neon"));
}

Теперь мы можем запустить наши тесты с помощью плагина покрытия мутаций, чтобы убедиться, что все мутации были уничтожены , как мы видим в отчете PITest, сгенерированном в целевом каталоге.

  • 100% покрытие линии: 7/7
  • 100% охват мутаций: 8/8

7. Конфигурация тестов PITest

Мутационное тестирование иногда может потребовать значительных ресурсов, поэтому нам необходимо установить правильную конфигурацию, чтобы повысить эффективность тестов. Мы можем использовать тег targetClasses , чтобы определить список классов, которые нужно изменить. Мутационное тестирование не может быть применено ко всем классам в реальном проекте, так как оно потребует много времени и ресурсов.

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

<configuration>
<targetClasses>
<param>com.foreach.testing.mutation.*</param>
</targetClasses>
<targetTests>
<param>com.foreach.mutation.test.*</param>
</targetTests>
<mutators>
<mutator>CONSTRUCTOR_CALLS</mutator>
<mutator>VOID_METHOD_CALLS</mutator>
<mutator>RETURN_VALS</mutator>
<mutator>NON_VOID_METHOD_CALLS</mutator>
</mutators>
</configuration>

Кроме того, библиотека PITest предлагает множество опций, доступных для настройки ваших стратегий тестирования . Вы можете указать максимальное количество мутантов, вводимых классом, например, с помощью параметра maxMutationsPerClass . Подробнее о параметрах PITest в официальном кратком руководстве по Maven .

8. Заключение

Обратите внимание, что покрытие кода по-прежнему является важной метрикой, но иногда этого недостаточно, чтобы гарантировать хорошо протестированный код. Итак, в этой статье мы рассмотрели мутационное тестирование как более сложный способ обеспечения качества тестов и одобрения тестовых случаев с использованием библиотеки PITest .

Мы также увидели, как анализировать базовые отчеты PITest при одновременном улучшении показателя охвата мутаций .

Несмотря на то, что мутационное тестирование выявляет дефекты в коде, его следует использовать с умом, поскольку это чрезвычайно затратный и трудоемкий процесс .

Вы можете ознакомиться с примерами, представленными в этой статье, в связанном проекте GitHub .