Статьи

Радость кодирования… и тестирование мутаций в Java

В течение многих лет хорошей практикой было написание модульных тестов для вашего исходного кода. А также использовать отчеты о покрытии тестов, чтобы увидеть, сколько кода покрыто тестами. Хотя отчеты о покрытии линии + ветки весьма полезны, они не говорят вам, насколько хороши ваши юнит-тесты. Следовательно, даже возможно достичь 100% покрытия без единого утверждения в ваших тестах.

Меня заинтересовали лучшие способы тестирования. Я присутствовал на семинаре «Тестирование мутаций» во время конференции «Радость кодирования» этого года .
Мутационное тестирование — это принципиально иной подход к выполнению и анализу результатов и охвата ваших юнит-тестов. Вместо того, чтобы измерять, к какому количеству вашего кода «обращаются» из ваших модульных тестов, он определяет, сколько вашего кода фактически «протестировано» вашими модульными тестами.

Так как это на самом деле работает

Основная идея мутационного тестирования состоит в том, чтобы внести небольшое изменение (мутацию) в (байтный) код, а затем выполнить ваши тесты, чтобы увидеть, обнаружено ли это модульными тестами.
Возможные мутации изменяют « > » на « >= », заменяя « ++ » на « -- » и удаляя вызовы метода « void ».
Каждая их мутация создает измененную версию вашего кода, которая называется «мутант».

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

Затем будут проводиться модульные тесты для каждого «мутанта» (что может занять очень много времени), если:

  • мутант обнаружен нашими модульными тестами: тесты не пройдены, и поэтому «мутант» считается «убитым».
  • мутант остается незамеченным в наших модульных тестах: тесты «не» провалились («мутант» считается «живым») и не заметили мутацию; это означает, что «мутант» на самом деле «не» проверен (раскрыт) с помощью модульных тестов.

Пример мутационного тестирования

Так как же на самом деле работает это «тестирование мутаций»?
Рассмотрим следующий метод:

1
2
3
4
5
6
7
public String foo(int i) {
    if ( i >= 0 ) {
        return "foo";
    } else {
        return "bar";
    }
}

И тот факт, что юнит-тесты состоят только из одного метода испытаний:

1
2
3
4
@Test
public void testFoo() {
    testee.foo(0);
}

Что если мы создадим «мутант» нашего кода, в котором « >= » заменяется на « > »?
Мы ожидаем, что наш метод модульного тестирования обнаружит это, верно? Ну, в данном случае это не так, поскольку метод теста не содержит ни одного утверждения.

Что бы мы изменили метод «testFoo», чтобы включить утверждение:

1
2
3
4
5
@Test
public void testFoo() {
    String result = testee.foo(0);
    assertEquals("foo", result);
}

Теперь наш метод модульного тестирования завершится с ошибкой и обнаружит (он же «убит») «мутантный» код.

Помимо изменения « >= » в « > » могут быть созданы дополнительные «мутанты»:

  • первый метод return можно изменить так, чтобы он возвращал null (вместо "foo" );
    этот «мутант» «убивается» методом «testFoo» из-за оператора «assertEquals», но остается незамеченным исходным методом «testFoo» (без каких-либо утверждений).
  • второй метод return можно изменить так, чтобы он возвращал значение null (вместо "bar" );
    поскольку ни один метод тестирования на самом деле не покрывает этот путь выполнения, этот «мутант» останется незамеченным.

ПРИМЕЧАНИЕ : некоторые инструменты тестирования мутаций (например, PIT для Java) даже не потрудятся создать «мутант» для второго оператора return поскольку он никогда не будет охватываться модульными тестами (как обнаруживается традиционным покрытием строк).

Эквивалентные мутации, вызывающие ложноположительные результаты

В отличие от традиционного покрытия линии + ответвления, охват мутациями может привести к ложноположительным результатам.
Он может «неверно» сообщить (ложно-положительный результат), что «мутант» как «не» обнаружен вашими юнит-тестами.

Например, рассмотрим следующий код Java:

1
2
3
4
5
public int someNonVoidMethod() { return 0; }
public void foo() {
  int i = someNonVoidMethod();
  // do more stuff with i
}

Во время мутационного тестирования (с использованием PIT Mutation-тестирования с некоторой «не» -конфигурацией по умолчанию) мог быть создан следующий «мутант»:

1
2
3
4
5
public int someNonVoidMethod() { return 0; }
public void foo() {
  int i = 0;
  // do more stuff with i
}

Оператор « int i = 0 » в «мутанте» функционально «эквивалентен» исходному коду, в котором « someNonVoidMethod » возвращает 0 .
Такая «эквивалентная мутация» не может быть обнаружена, поскольку модульные тесты не будут (и должны) проваливаться на ней.
И поэтому будет сообщено, что он не покрыт, тогда как на самом деле он является ложноположительным.

При использовании PIT, среды тестирования мутаций для Java, «эквивалентные мутации» должны, согласно документации , быть минимальными с использованием набора «по умолчанию» мутаторов.
Например, «Мутатор вызова не Void метода» PIT, вызывающий « int i = 0 » эквивалентную мутацию, по умолчанию отключен.

Вывод

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

Если вы также заинтересовались:

  • Посмотрите эту очень забавную презентацию Криса Риммера об основной концепции мутационного тестирования.
  • кроме того, есть интересная статья от компании TheLadders, в которой используется инструмент тестирования мутаций PIT.
  • также есть обширная статья Филипа ван Лаенена о «тестировании мутаций» в выпуске 108 журнала перегрузки.
  • И последнее, но не менее важное: на веб-сайте тестирования мутаций PIT есть документация .