Выпуск 25. Март 2015
← Подключение в Mojolicious модели для бизнес-логики | Содержание | Про переменные и сигнатуры в Perl 6 →Мутационное тестирование
Еще один способ сделать Perl-код качественнее — мутировать тесты для нахождения непротестированного кода
Мутационное тестирование — способ проверки качества тестовых наборов путем внесения случайных изменений в тестируемый код. Если после таких изменений тесты все еще проходят, значит они недостаточно качественны, т.е. не полны.
Чтобы понять, зачем это нужно и как это проверять, рассмотрим следующий пример:
package Temp;
use strict;
use warnings;
sub new {
my $class = shift;
my (%params) = @_;
my $self = {};
bless $self, $class;
$self->{critical} = $params{critical};
return $self;
}
sub is_critical {
my $self = shift;
my ($temp) = @_;
return $temp >= $self->{critical};
}
1;
Данный класс возвращает флаг, сигнализирующий о критической температуре. Тест для него был написан не очень внимательным программистом и выглядит следующим образом:
use strict;
use warnings;
use Test::More;
use Temp;
my $temp = Temp->new(critical => 32);
ok $temp->is_critical(32);
done_testing;
Тест явно недостаточен. Проверяется только лишь равенство критической температуре. Как же выявить, что тест неполный? Попробуем посмотреть на покрытие кода тестами, используя Devel::Cover:
$ PERL5OPT=-MDevel::Cover prove t && cover
lib/Temp.pm 100.0 n/a n/a 100.0 0.0 100.0
Покрытие 100%. Таким способом мы ничего не выявили. Неужели нельзя каким-то образом автоматически выявлять подобные проблемы, без участия человека? Можно! Здесь нам и пригодится мутационное тестирование.
Задача мутационного тестирования на основе исходного класса, скрипта, да вообще любого кода, — сгенерировать несколько вариантов, где случайным образом изменить операторы, ключевые слова, удалить или добавить переменные и так далее. Затем мутированный код запускается через исходный набор тестов, и если тесты проходят, значит они недостаточно качественны и полны.
В описанном примере при мутации оператор >=
будет заменен, например, на <=
, и тесты все также будут проходить, таким образом выявляя свои недостатки.
На сегодняшний день для многих языков программирования существуют подсобные утилиты для проведения мутационного тестирования. Здесь же рассмотрим, как это можно сделать в Perl.
Как отпарсить Perl
Как можно проводить мутации? Можно, конечно, использовать регулярные выражения, однако на практике разбирать Perl-код с помощью регулярных выражений невозможно без высокой доли ложных срабатываний, ошибок и прочего. Гораздо более надежный способ — это использовать PPI.
Несмотря на известное выражение “Only perl can parse Perl” (“Только perl-интерпретатор может отпарсить Perl-язык”), PPI работает довольно сносно для большинства примеров кода. Вот как, например, заменить в описанном выше классе оператор >=
на <=
:
my $ppi = PPI::Document->new('Temp.pm');
if (my $operators = $ppi->find('PPI::Token::Operator')) {
foreach my $operator (@$operators) {
if ($operator->content eq '>=') {
$operator->set_content('<=');
$ppi->save('Temp.pm.mutant');
}
}
}
Т.е. находим все операторы (токен PPI::Token::Operator
), затем нужный заменяем на <=
и сохраняем мутанта под новым именем. Можно проводить и несколько мутаций, таким образом выявляя практически непроявляющиеся ошибки.
Для мутационного тестирования на CPAN есть модуль Devel::Mutator
. Для генерации мутантов необходимо указать, какие файлы мутировать:
$ mutator mutate -r lib/*
По умолчанию все мутанты складываются в директорию mutants
. Ключ -r
для рекурсивного нахождения модулей.
Для запуска же самих тестов выполняется команда test
:
$ mutator test
Это команда для каждого мутанта из директории mutants
запускает тесты из текущей директории. По умолчанию, запускается prove -l t
, но это можно переопределить с помощью опции --command
. Успешность или неуспешность тестов проверяется по значению $?
. Т.е. если вы используете нестандартные тестовые модули, достаточно делать exit(0)
в случае успеха или exit(255)
или любое другое значение при неудаче.
Во время выполнения тестов на экран выводится текущий мутант и статус выполнения тестирования, где ok
означает, что тесты не проходят, а not ok
— наоборот. Например:
$ mutator test
(1/10) ./mutants/49606d6...009cd7a2/MyClass.pm ... ok
(2/10) ./mutants/3f9577f...3fd5f5b9/MyClass.pm ... not ok
(3/10) ./mutants/a7c40ad...f05df2e4/MyClass.pm ... not ok
(4/10) ./mutants/514abf0...8401c2f3/MyClass.pm ... not ok
...
Если мутант был запущен неуспешно, можно просто посмотреть, что было изменено и как это повлияло на тесты:
diff lib/MyClass.pm mutants/49606d6bcaaf550b6cf76abf009cd7a2/MyClass.pm
Из diff
можно понять правильно ли была выполнена мутация, стоит ли обращать на это внимание. diff
может показываться и автоматически, если указать опции -v
, так можно сразу видеть, где проблема:
$ mutator test -v
(1/10) ./mutants/3f9577f3d44ac20732b6340d3fd5f5b9/MyClass.pm ... not ok
628c628
< my @related = @_ == 1 ? ref $_[0] eq 'ARRAY' ? @{$_[0]} : ($_[0]) : ({@_});
---
> my @related = @_ != 1 ? ref $_[0] eq 'ARRAY' ? @{$_[0]} : ($_[0]) : ({@_});
После выполнения всех тестов выводится общий результат. Например:
Result: FAIL (31/38)
или
Result: PASS
Таким образом с помощью модуля Devel::Mutator
можно протестировать сами тесты и понять, как их можно улучшить, тем самым повысив качество программы в целом.
Недостатки
Существенным недостатком мутационного тестирования является возможность после мутации получить эквивалентную программу. В этом случае тесты будут проходить, и произойдет ложное срабатывание. Нахождение эквивалентных программ чрезвычайно сложно и на данный момент нерешаемо. Есть несколько стратегий борьбы с ложными срабатываниями, написаны несколько десятков технических статей, но универсальное решение так и не предложено.
Еще одним недостатком является вероятность создания бесконечных циклов. Для борьбы с этим в Devel::Mutator
тесты запускаются с timeout, который по умолчанию равен 10 секундам.
← Подключение в Mojolicious модели для бизнес-логики | Содержание | Про переменные и сигнатуры в Perl 6 →