7. Темная сторона юнит-тестов
int multiply(int a, int b) {
return a * b;
}
- - - - - - - - - - - - - - - - - - - - -
Failed: 0
Passed: 1
Code coverage: 0
void testMultiply() {
// Необходимо создать тест? Вот он :)
Assert.assertTrue(true);
}
8. int multiply(int a, int b) {
return a * b;
}
- - - - - - - - - - - - - - - - - - - - -
Failed: 0
Passed: 2
Code coverage: 100%
void testMultiply2() {
Assert.assertTrue(multiply(2, 3) > 0);
}
Темная сторона юнит-тестов
9. int multiply(int a, int b) {
return a * b;
}
- - - - - - - - - - - - - - - - - - - - -
Failed: 0
Passed: 3
Code coverage: 100%
void testMultiply3() {
multiply(2,3);
}
Темная сторона юнит-тестов
10. int multiply(int a, int b) {
return a * b;
}
- - - - - - - - - - - - - - - - - - - - -
Failed: 0
Passed: 4
Code coverage: 100%
void testMultiply4() {
multiply(2,3);
Assert.assertTrue(true);
}
Темная сторона юнит-тестов
11. Что это за животное?
Мутационное тестирование (analysis/мутирование) -
основывающаяся на внедрении ошибок, техника
тестирования ПО, которая оценивает качество тестов
посредством изменения частей проверяемого кода
Обнаружение до 70%-90% ‘реальных’ ошибок
16. Conditionals
if (a < b) {
// do something
}
if (a <= b) {
// do something
}
if (a == b) {
// do something
}
if (true) {
// do something
}
Math
int a = b + c; int a = b - c;
public class A {
private int i;
public void foo() {
this.i++;
}
}
public class A {
private int i;
public void foo() {
this.i = this.i - 1;
}
}
Что это за животное?
17. Return values
public Object foo() {
return new Object();
}
public Object foo() {
new Object();
return null;
}
public void someVoidMethod(int i) {
// does something
}
public int foo() {
int i = 5;
doSomething(i);
return i;
}
public void someVoidMethod(int i) {
// does something
}
public int foo() {
int i = 5;
return i;
}
Increment
public int mtd(int i) {
i++;
return i;
}
public int mtd(int i) {
i--;
return i;
}
Void
methods
Что это за животное?
18. ● Гарантировать достаточность покрытия кода юнит-тестами
○ Увеличить понимание правил кода среди разработчиков
■ Получить точные метрики покрытия кода
● Обезопасить код построчной, ветвлённой проверкой
○ Повысить общее качество продукта
Цели
20. Pros Cons
Полное покрытие исходного кода
Тщательное тестирование мут-ов
Определение неоднозначностей
Скорость*
Open-source инструменты
TDD-ориентация
Инкрементальный анализ
Высокая стоимость и
времязатратность (геометрич.
прогрессия): количество данных
Должно быть автоматизировано
Только “белый ящик”
Необходимость множества тестов,
для отличия мутанта от исходного
кода
ExcuseMeIwriteGoodTests();
26. 34: 5. mutated return of Object value /*...*/ → SURVIVED
public String getMachineById(long nodeId) {
if(nodeId2machine.containsKey(nodeId)) {
if(nodeId2machine.get(nodeId) != null) {
return null;
} else {
throw new RuntimeException();
}
} else {
return DEFAULT_MACHINE_NAME;
}
}
27. 34: 1. negated conditional → KILLED
34: 2. mutated return of Object value /*...*/ → KILLED
@Test
public void testGetMachineById3() {
NodeMapper nodeMapper = new NodeMapper(MACHINES);
Assert.assertEquals(NodeMapper.DEFAULT_MACHINE_NAME, nodeMapper.getMachineById(0));
Assert.assertEquals(TEST_MACHINE_NAME, nodeMapper.getMachineById(1));
}
28. ● Недостаточность юнит-тестов и надежность code-coverage метрик
● Мутационное тестирование - тесты для тестов
● Широкий спектр мут-нных операторов => мутантов
● Необходимость детальной настройки для больших проектов
● “Точечные” запуски в целях экономии времени
● Выявление неявных ошибок в коде и тестах
● Точные метрики покрытия
Выводы
Кратко вспомним об уровнях тестирования
Слабые стороны юнит-тестирования
Что такое мутационное тестирование?
Основные цели, как причины для использования
Доступные инструменты на разных языках
Сильные стороны и недостатки
Примеры
Вопросы
Вне поля нашего рассмотрения - организация и настройка соответствующей инфраструктуры для этого вида тестирования.
Чтобы лучше понять место, занимаемое мутационным тестированием, полезным было бы вспомнить уровни тестирования. Понимая, что основной целью QA, использующего наиболее эффективные инструменты, техники и подходы, является удостоверение в том, что продукт содержит как можно меньше ошибок, т.е. его качество соответствует согласованным нормам, (CLICK 1) прежде всего, важно убедиться в том, что каждый индивидуальный юнит (неделимая единица бизнес логики) выполняет ожидаемую от него функцию. Такой уровень мы называем “юнит” и/или “модульным”. Тем не менее, даже не смотря на скорость и самые низкоуровневые проверки, ожидаемый результат далеко не всегда может соответствовать реальности (NEXT SLIDE)
Эти гифки можно было бы назвать как на ролики ютубе: “Вы бы не поверили, если бы это не было записано” (“You would not believe it if it was not recorded”)
Моей минимальной рекомендацией к разработчикам, было бы хотя бы попытка локально сбилдить артефакт перед пушем своих изменений в общий репозиторий. (помню обновился на последний коммит в ветке, которой велась разработка, попытка сбилдить, fail, лишь из-за того что разработчик оставил в новой строке кавычки (использующиеся для строк), сами по себе).
(CLICK 1) Чтобы проверить, как отдельные модули взаимодействуют и обмениваются данными друг с другом, мы используем интеграционные тесты.
(CLICK 2) Для верификации поведения модулей в соответствии с функциональными требованиями с точки зрения конечного пользователя, мы используем системное тестирование
Каждый из вышеупомянутых уровней предполагает собственные метрики, выраженные в различных числовых показателях, как и других индикаторах. Одна из наиболее важных для разработчика - покрытие кода (code coverage). Покрытие кода, конечно же, может нам сообщить нечто о его качестве, особенно, если для кода нет или не было написано никаких тестов. И, тем не менее, высокий показатель покрытия кода, совершенно еще никак не гарантирует высокое качество этого же кода. Как следствие, такая метрика как “покрытие кода”, нередко может вводить разработчиков в заблуждение, или даже, самообман.
На следующих слайдах показано, как делать НЕ СТОИТ. Надеюсь, что мы не малые дети, которые на подобное утверждение, хотят сделать всё с точностью до наоборот.
Представим, что заказчик или БА попросил ваc написать функцию для умножения двух чисел, и согласно нормам разработки, использующимся в компании, вы должны покрыть эту функцию тестами.
(CLICK) Вуаля, вы замечательны! Две минуты, всё готово! Тест проходит, ничего не фейлится, НО покрытие кода (который на джаве можно удобно выполнять с помощью библиотеки jacoco) на нуле. В таком случае, заказчика или БА, возможно мы и обведём вокруг пальца, однако скорее всего не надолго.
Оптимизируем попытки… Ведь метрики покрытия кода, можно легко обхитрить.
(CLICK1) Тест проходит, фейлов нет, и покрытие кода 100%. Наконец-то! Однако, в случае рефакторинга (например, по неизвестной причине будет добавлен минус), или же при передаче других параметров (нуля, или отрицательного числа) мы наверняка получим падающий тест или просто недостаточное количество тестов.
Другой пример обхода покрытия кода - (CLICK) вызов в тесте только лишь функции, без проверок.
И еще один lifehack, который позволяет не только обхитрить инструменты покрытия кода, но и самых внимательных код ревьюиров/тестировщиков, (CLICK) посредством вызова проверяемой функции, которая бы проходилась по условиям и неправильно сделанного ассерта.
Итак теперь вы можете сдать экзамен начального уровня по хитростям в юнит-тестировании :)
В сущности, через показанные примеры мы ставим под сомнение качество тестов (не качество продукта), а именно:
Аккуратны или метрики покрытия кода?
Можем ли мы быть уверены в том, что тесты действительно покрывают код?
Проходят ли тесты по всем ветвлениям условий (code branches)?
Хорошее ли качество и достаточность тестов?
Техника мутационного тестирования не только дает ответы, но непосредственно позволяет предотвратить и избежать отрицательных ответов на вышеозвученные вопросы.
КРАТКАЯ ИСТОРИЯ:
Происхождение техники уходит корнями в далекий 1971 год к имени Ричарда Липтона, однако первая имплементация инструмента для мутационного тестирования была сделана спустя почти 10 лет, в 1980 году Тимоти Баддом из Йельского Университета в его диссертации “Мутационный анализ” (с использованием FORTRAN). Интересно то, что Мутационное тестирование начало набирать популярность и все более широко использоваться только за последнее десятилетие (с 2010гг).
Существует множество определений, однако одно из наиболее простых гласит: мутационный анализ использует набор хорошо определенных правил для синтаксических структур, для того, чтобы делать систематические изменения внутри артефактов ПО.
(CLICK) Некоторые из современных исследований показывают, что Мутационное тестирование способно обнаруживать до 70%-90% реальных ошибок.
Основные уровни затрагиваемые техникой мутационного тестирования
(CLICK1) Unit
(CLICK2) Integration
Давайте кратко рассмотрим алгоритм, назовем его “жизненный цикл мутационного тестирования”:
Сбилдить исходный код (программу)
(CLICK) запустить тесты (стрелки)
(CLICK) Мутировать скомпилированный байткод (посредством изменений инструкций, условий, математических операторов, переменных, и т.д. PITtest использует ASM библиотеку для манипуляций с байткодом). Каждая версия с изменением называется мутантом.
Запустить тесты на мутированном коде. Ошибки (или мутанты) автоматически помещаются в код, затем запускается существующий тест для этого кода - процесс называется “убиванием” мутанта.
Получение результатов: (CLICK) если тесты упали (отреагировали на изменение кода), значит мутант считется убитым (убитые мутанты) (т.е. если изначальная программа и мутированная программа генерируют различные выходные данные в результате тестов, тогда можно говорить, что тест способен убить мутанта, поскольку он настолько хорош, что способен определить изменения между изначальным (правильным) и измененным (неправильным) кодом. (CLICK) В обратном же случае, если тест не упал, значит мутант выжил (выжившие мутанты).
Итак закрепим информацию:
Для того, чтобы тест мог убить этого мутанта, необходимо чтобы были выполнены следующие условия, RIP model:
Reachability: Тест должен быть способен достигнуть мутированного оператора/инструкции.
Infection: Infect the program state with the test data so it could result in incorrect state, that is, test input must produce different behavior on mutated and not mutated tested code (Входные данные теста должны привести к разным состояниям программы-мутанта (Infect) и исходной программы. Например, тест с a = 1 и b = 0 приведет к этому).
Propagation: The incorrect state propagates to incorrect output, or, in other words, incorrect state must be checked by the test (Значение переменной c должно повлиять (Propagate) на вывод программы и быть проверено тестом).
If only the first two point satisfied - weak killing of mutants (easier to kill mutants under this assumption and requires less analysis), if all three - strong killing of mutants.
Практичный вопрос: Что же обозначает наличие выжившего мутанта? Не так уж много возможных вариантов:
a) необходимо улучшить тесты (в большинстве случаев)
b) внести изменения в исходный код
c) ограничить используемые мутаторы, чтобы не получать ложно-положительные срабатывания (false positives).
По завершению тестов мы получаем мутационный балл - количество убитых мутантов разделенное на общее количество мутантов
Мутирование кода происходит за счет Мутационных операторов. Мутационный оператор - это правило изменения исходного кода, а примененное правило - мутант.
Сразу же стоит ответить, что применять все возможные операторы нет необходимости, поскольку, использование этой техники станет еще одной головной болью.
(CLICK) Conditionals boundary (replaces relational operators);
(CLICK) Increments;
(CLICK) Invert negatives;
(CLICK) Math (instead of concatenated strings StringBuilder may be used);
(CLICK) Negate conditionals;
(CLICK) Return values;
(CLICK) Void methods
Other mutators usually deactivated by default:
Constructor calls;
Inline constants (mutates inline constants);
Null returns mutator;
Non void method calls;
Remove conditionals;
Experimental member variable (mutates classes by removing assignments to member variables, The members will be initialized with their Java Default Value for the specific type.);
Experimental switch (finds the first label within a switch statement that differs from the default label. It mutates the switch statement by replacing the default label (wherever it is used) with this label. All the other labels are replaced by the default one)
Mutants are based on well-defined mutation operators that either mimic typical programming errors (such as using the wrong operator or variable name) or force the creation of valuable tests (such as dividing each expression by zero).
Несколько примеров применения мутационных операторов.
Имея общее представление о том, что такое Мутационное тестирование, можно указать несколько основных целей, которые можно рассматривать причинами для использования данной техники:
(CLICK) Гарантировать достаточность покрытия кода юнит-тестами
(CLICK) Увеличить понимание правил кода среди разработчиков
(CLICK) Получить точные метрики покрытия кода
(CLICK) Обезопасить код построчной, ветвлённой проверкой (line, branch mutation walking)
(CLICK) Увеличить общее качество продукта
JS: Stryker
Python: Cosmic Ray, Mutmut
CONS:
(CLICK) mutation testing is an extremely costly and time-consuming process because all the programs mutants should be separately generated. Worth to mention, however, there is no need to run mutation testing every time for the whole code base (it is enough to run it for the class we are writing tests for) and it is solvable by smart mutants’ selection;
(CLICK) This type of testing should be automated, since afterwards activities and analysis takes a lot of human attention and time;
(CLICK) Since this method involves changing the source code, it is not applicable for the black-box testing
(CLICK) Not always clear and easy to distinguish the mutant from the original SW
(CLICK) Do you know what is this point about? The most annoying problem is available to any developer the static function that may be accessed at any time from anywhere “Excuse me, I write good tests”
PROS:
(CLICK) Mutation testing allows to cover the entire source code;
(CLICK) Program mutants are thoroughly tested;
(CLICK) Easily defines ambiguities in the source code
(CLICK) Overall performance (depending on the selected mutants, tests...)
(CLICK) Free usage since tools are open-source
(CLICK) TDD-oriented
(CLICK) Incremental analysis (experimental feature to enable its use on very large codebases. If this option is activated, PIT will track changes to the code and tests and store the results from previous runs. It will then use this information to avoid re-running analysis when the results can be logically inferred (predictable). There are possible error, that may be caused not src code but by dependencies)
Worth to mention not testable mutations:
(original code)
String isBigOrSmall(int x) {
if x > 0
return “larger”;
else if x < 0
return “smaller”;
return “ ”;
}
(Mutation)
- else if x < 0
+ else if x != 0
=> such mutation is not testable, since the logic itself (as well as tests) is mutually exclusive, x != 0 is equivalent to ((x < 0) & (x > 0)).
Let’s take a class with a simple method which maps machine to be used in different nodes. This is an example taken from public sources not SwissQuote projects.
This is an example of the very bad test, that may be written by the one who just want to make the appearance that a test was written. However, such workaround can simply be undiscovered by code coverage tools (like jacoco).
The following slide show that PIT has change source code, by reverting expected result of condition, so called ‘negated conditional mutant’ and it survived, and no test failed after such mutations
The other mutation is related to return value, which was changed to conditional statements and the test could not kill it
So, as soon as the tests are failing against new mutants, they are considered as sufficient, and, correspondingly, mutants - killed.
Show reports page of PIT (in the ‘pit-reports’ folder)
Недостаточность юнит-тестов
Ненадежность code-coverage метрик
Мутационное тестирование - тесты для тестов
Широкий спектр мут-нных операторов => мутантов
Необходимость детальной настройки для больших проектов
“Точечные” прогоны разработчиками
Выявление неявных ошибок в коде и тестах
Точные метрики покрытия
Недостаточность юнит-тестов
Ненадежность code-coverage метрик
Мутационное тестирование - тесты для тестов
Широкий спектр мут-нных операторов => мутантов
Необходимость детальной настройки для больших проектов
“Точечные” прогоны разработчиками
Выявление неявных ошибок в коде и тестах
Точные метрики покрытия
Недостаточность юнит-тестов
Ненадежность code-coverage метрик
Мутационное тестирование - тесты для тестов
Широкий спектр мут-нных операторов => мутантов
Необходимость детальной настройки для больших проектов
“Точечные” прогоны разработчиками
Выявление неявных ошибок в коде и тестах
Точные метрики покрытия
Недостаточность юнит-тестов
Ненадежность code-coverage метрик
Мутационное тестирование - тесты для тестов
Широкий спектр мут-нных операторов => мутантов
Необходимость детальной настройки для больших проектов
“Точечные” прогоны разработчиками
Выявление неявных ошибок в коде и тестах
Точные метрики покрытия
Недостаточность юнит-тестов
Ненадежность code-coverage метрик
Мутационное тестирование - тесты для тестов
Широкий спектр мут-нных операторов => мутантов
Необходимость детальной настройки для больших проектов
“Точечные” прогоны разработчиками
Выявление неявных ошибок в коде и тестах
Точные метрики покрытия