Статьи

Что, черт возьми, тестирование мутаций?

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

Что такое мутационное тестирование?

Мутационное тестирование оценивает качество существующих тестов программного обеспечения. Идея состоит в том, чтобы немного изменить (преобразовать) код, охватываемый тестами, и проверить, обнаружит ли существующий набор тестов отклонение изменения [MUTTES]. Если это не так, это означает, что тесты не соответствуют сложности кода и оставляют один или несколько его аспектов непроверенными.

В Java думать о мутанте как о дополнительном классе с единственной модификацией по сравнению с исходным кодом. Это может быть изменение логического оператора в предложении if , как показано ниже.

1
if( a && b ) {...} => if( a || b ) {...}

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

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

Тестирование с помощью JUnit

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

Фрэнк написал книгу, которая дает глубокое представление о основах тестирования с JUnit и готовит вас к ежедневным трудным задачам, связанным с тестированием.

Учить больше…

Как это связано с охватом кода?

«Тестовое покрытие — это полезный инструмент для поиска непроверенных частей кодовой базы», как говорит Мартин Фаулер [TESCOV]. Это означает, что плохие цифры охвата указывают на тревожные дыры в сети безопасности тестового набора Однако одно только полное покрытие ничего не говорит о качестве базовых тестов! Единственный разумный вывод, который можно сделать, состоит в том, что явно нет открытых пятен.

Чтобы прояснить этот момент, рассмотрим набор тестов, которые, например, полностью пропускают этап проверки . Хотя такой пакет мог бы обеспечить полное покрытие кода, он, по-видимому, был бы довольно бесполезным с точки зрения обеспечения качества. Это где тестирование мутаций вступает в игру.

Чем больше мутантов убит набор тестов, тем больше шансов, что поведение производственного кода было хорошо продумано и полностью охвачено надежными тестами. Звучит заманчиво? Тогда давайте продолжим и посмотрим на пример, чтобы получить представление о практическом применении.

Как это используется?

Мы начнем с листинга, который я позаимствовал из первой главы моей книги « Тестирование с JUnit», и немного изменим его для реального контекста. Думайте о временной шкале как о компоненте модели элемента управления пользовательского интерфейса, который показывает записи списка в хронологическом порядке, как, например, интерфейс Twitter. На этом этапе мы заботимся только о переменной состояния fetchCount , начальное значение которой можно изменить с помощью натуральных чисел.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Timeline {
 
  static final int DEFAULT_FETCH_COUNT = 10;
   
  private int fetchCount;
   
  public Timeline() {
    fetchCount = DEFAULT_FETCH_COUNT;
  }
 
  public void setFetchCount( int fetchCount ) {
    if( fetchCount <= 0 ) {
      String msg = "Argument 'fetchCount' must be a positive value.";
      throw new IllegalArgumentException( msg );
    }
    this.fetchCount = fetchCount;
  }
 
  public int getFetchCount() {
    return fetchCount;
  }
}

Хотя здесь нет ничего сложного, мы чувствуем себя уверенно, имея следующий тестовый пример (давайте обратимся к различным методам assert встроенного в org.junit.Assert класса org.junit.Assert для проверки в этом посте, примененного для краткости со статическим импортом. ).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TimelineTest {
   
  private Timeline timeline;
 
  @Before
  public void setUp() {
    timeline = new Timeline();
  }
   
  @Test
  public void setFetchCount() {
    int expected = 5;
 
    timeline.setFetchCount( expected );
    int actual = timeline.getFetchCount();
 
    assertEquals( expected, actual );
  }
   
  @Test( expected = IllegalArgumentException.class )
  public void setFetchCountWithNonPositiveValue() {
    timeline.setFetchCount( 0 );
  }
}

Действительно, выполнение тестов при сборе данных покрытия с помощью EclEmma приводит к полному отчету о покрытии, как показано на следующем рисунке.

Сроки испытаний покрытия

Возможно, вы уже обнаружили слабое место. Но давайте играть наивно, игнорировать облака на горизонте и продолжить мутационный тест. Для этой цели мы используем PIT, так как он кажется наиболее популярным и наиболее активно поддерживаемым инструментом в этой области. Другие возможности будут µJava и Jumble .

PIT поддерживает выполнение командной строки , интеграцию Ant и Maven , а также интеграцию IDE и отчетов сторонними разработчиками . Для получения более подробной информации о различных сценариях использования, пожалуйста, обратитесь к соответствующей онлайн-документации.

Сгенерированный HTML-отчет о тестировании мутации для конкретного проекта содержит разбивку пакета и может быть детализирован до уровня класса. На следующем рисунке показан отчет о списке классов нашего компонента временной шкалы. Ниже тот же отчет показан в виде структурированного дерева в Eclipse IDE.

график-мутация с-уцелевший

Какой шок! Наша уверенность в высоких показателях охвата была заблуждением. Как видите, в отчете перечислены мутации, которые были применены к какой строке. Опять же, помните, что для каждой мутации выполняется отдельный тестовый запуск, включая все тесты! Зеленые подчеркнутые записи в списке обозначают убитых мутантов, а красные — выживших.

При ближайшем рассмотрении довольно скоро становится ясно, что мы пропустили. Мы позаботимся о проблеме, добавив проверку начального состояния в наш тестовый пример, как показано в следующем фрагменте (обратите внимание на статический импорт Timeline.DEFAULT_FETCH_COUNT ).

01
02
03
04
05
06
07
08
09
10
11
public class TimelineTest {
   
  [...]
   
  @Test
  public void initialState() {
    assertEquals( DEFAULT_FETCH_COUNT, timeline.getFetchCount() );
  }
 
  [...]
}

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

Сроки мутация-без оставшихся в живых

Трудно поверить количеству мутаций, созданных для такого маленького класса. 9 мутантов всего за 22 инструкции! Что приводит нас к последнему разделу этого поста.

Каковы недостатки?

Анализ покрытия в восходящем направлении, создание мутанта на лету и все необходимые тестовые прогоны занимают довольно много времени. Я включил мутационное тестирование в прогон сборки полного примера приложения на временной шкале, в котором содержится около 350 тестов. Это увеличило время выполнения в 4 раза по сравнению с обычным прогоном.

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

В ходе полевых испытаний появилась еще одна проблема, показавшая, что PIT может столкнуться с проблемами с базовым технологическим стеком [STAPIT]. В моем случае, казалось, что Runst- тест Burst JUnit, используемый для параметризованных тестов на основе перечисления, не поддерживается. Из-за этого все мутации конкретного тестируемого класса выжили. Но ручное воспроизведение подтвердило, что эти результаты были неверными. Таким образом, вы либо обходитесь без проблемной технологии, либо настраиваете PIT для исключения проблемных тестовых случаев.

Резюме

Этот пост дал краткое введение в тестирование мутаций. Мы узнали, что такое тестовые мутанты, как показатель убийств мутантов определяет качество существующего набора тестов и как эта методика тестирования связана с охватом кода. Кроме того, мы увидели, как работать с PIT, наиболее популярным инструментом в этой области, и провели оценку некоторых отчетов об исполнении. Тема была завершена с учетом некоторых недостатков, выявленных в ходе полевых испытаний.

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

использованная литература

Ссылка: Что, черт возьми, тестирование мутаций? от нашего партнера JCG Фрэнка Аппеля в блоге Code Affine .