Выпуск 14. Апрель 2014
← От редактора | Содержание | Pjam — сервер сборки перловых приложений →Тестирование в Perl. Лучшие практики
Рассмотрены основные практики для улучшения качества тестирования в Perl
В моей компании мы очень серьезно относимся к тестированию. За годы работы и реализации проектов различной сложности накопилось некоторое количество лучших практик, которые помогают сэкономить на запуске, поддержке и внедрении решений для бизнеса. Для разработчиков это существенно упрощает проведение рефакторинга, поиска багов, мотивирует к написанию более качественного кода. У нас нет отдела тестирования, сам процесс разработки и является тестированием.
В данной статье мы рассмотрим разработку через тестирование. Разумеется, это лишь часть процесса и никак не исключает других видов тестирования (функциональное тестирование, тестирование безопасности и прочее). Везде, где не указано иное, подразумевается юнит-тестирование (тестирование отдельных классов).
Основные преимущества разработки через тестирование
Интерфейс создается автоматически.
Нет необходимости продумывать интерфейс класса. Он сам собой вырисовывается во время использования (тестирования).
Имплементируется только необходимое.
Нет кода, который не используется. Все, что не протестировано, выкидывается.
Система разрабатывается небольшими шагами.
Нет возможности реализовать большой кусок кода, так как всегда требуется написать тест до самого кода.
Поощряется написание модульного кода.
Модульные тесты пишутся на классы. Чем несвязаннее классы — тем легче их тестировать.
Ошибки проектирования выявляются как можно раньше.
Во время тестирования на ранних этапах проявляются все сложности и неудобства неправильного проектирования, будь-то сложность работы класса или подсистемы с другими классами и подсистемами, невозможность тестирования или так далее.
Модуль работает.
Есть уверенность в том, что модуль хоть как-то работает до его внедрения.
Возможность рефакторинга.
При наличии тестовой базы рефакторинг становится безопасным. Изменение поведения и наличие регрессий контролируется тестами.
Основные недостатки разработки через тестирование
Одни и те же ошибки могут быть оставлены как в коде, так и в тестах.
Тест может тестировать неправильную логику работы модуля.
Ложная уверенность в работоспособности системы.
При наличии большого количества тестов может сложиться ложная уверенность в отсутствии ошибок в системе.
Больше кода для поддержки
Тесты это тоже код и его нужно поддерживать. Это время.
Тесты могут быть хрупкими и ломаться при каждом незначительном изменении кода.
Большинство недостатков можно побороть, следуя лучшим практикам.
Лучшие практики
Следование циклу разработки
Разработка через тестирование должна следовать циклу: красный, зеленый, рефакторинг. Что обычно означает:
Написать тест и убедиться, что тест не проходит.
Это необходимо для избежания случаев, когда тест неправильный. Например, вы нашли ошибку в коде, знаете как ее исправить, написали тест, но не убедились, что именно этот тест выявляет ошибку и исправили код.
- Написать класс и убедиться, что тест проходит.
Отрефакторить написанный код.
Это касается и класса, и теста.
Почему возникла ошибка? Достаточно ли читабелен сам код?
Часто из-за добавленного нового теста возникает ненужное дублирование, которое проще исправить сейчас, а не ждать, пока оно разойдется по другим тестам.
Тесты должны быть простыми и понятными
Тесты, как уже упоминалось, это тоже код. Он должен быть как можно проще и понятнее. Если не рефакторить и не улучшать тесты, они превратятся в кашу, и все их преимущества сойдут на нет.
Тесты не должны зависеть друг от друга
Тесты должны быть максимально независимыми. У каждого теста должна быть независимая подготовка окружения и данных. Таким образом будет тестироваться только нужный функционал без побочных эффектов.
Тесты должны быть сгруппированы
У каждого тестового случая (не путать с тестом на класс) должна быть своя область видимости для избежания влияния тестов друг на друга. Очень часто приходится видеть подобные тесты:
my $foo = Foo->new(name => 'Name', last_name => 'Last Name');
ok($foo);
is($foo->name, 'Name');
is($foo->last_name, 'Last Name');
Гораздо лучше написать тест следующим образом:
subtest 'create new object' => sub {
my $foo = Foo->new;
ok($foo);
};
subtest 'correcly initialize object' => sub {
my $foo = Foo->new(name => 'Name', last_name => 'Last Name');
is($foo->name, 'Name');
is($foo->last_name, 'Last Name');
};
Таким образом четко видно, какой тестовый случай что покрывает, легко вносить изменения и добавлять новые тесты.
Один тест — одна проверка
Каждый тестовый случай должен проверять единственную функциональность. Таким образом никогда не возникнет тестов, которые тестируют “все”, обычно они называются test_general
, test_ok
и тому подобное. В предыдущем примере четко видно, что каждый субтест тестирует конкретную возможность класса.
3A, AAA, Arrange-Act-Assert
По-русски это звучит примерно как Подготовка, Выполнение, Проверка. Тесты должны быть организованы в таком порядке:
- подготовить данные, объект и тому подобное для тестирования;
- вызвать на объекте нужный метод или каким-то образом запустить тестируемый функционал;
проверить, что он отработал правильно.
subtest 'correcly concatenates two strings' => sub { # Подготовка my $concatenator = Concatenator->new; # Выполнение my $result = $concatenator->cat('foo', 'bar'); # Проверка is $result, 'foobar'; };
Это делает тесты проще. Легко видеть что тестируется, в каких условиях.
Тестировать поведение, а не конкретную реализацию
Для избежания хрупких тестов, когда малейшее изменение в коде ломает существующие тесты, рекомендуется писать тесты на поведение. Например:
eval { $object->die_hard };
is "$@", "We died here for the good reason. Error 42";
В данном случае малейшее изменение текста ошибки приведет к сломанному тесту. В нашем случае, мы тестируем, что в тексте ошибки должно присутствовать Error <код-ошибки>
.
eval { $object->die_hard };
like "$@", qr/\s* Error \s+ \d+$/xms;
Не увлекаться mock-объектами
Mock-объекты — это объекты, которые можно использовать вместо настоящих объектов, когда их создание невозможно, сложно или они еще не реализованы.
Сами mock-объекты заслуживают своей отдельной статьи, здесь лишь скажем, что не стоит их использовать повсеместно. Основной недостаток mock-объектов проявляется при изменений поведения или интерфейсов настоящих объектов. В данном случае ваши старые тесты будут проходить и дальше, даже несмотря на то, что вы поменяли метод foo
на bar
в настоящем классе.
Рекомендуется использовать mock-объекты в режиме подстановки отдельных методов на настоящих объектах, например, используя Test::MonkeyMock
:
my $mocked_url_fetcher = Test::MockObject->new;
$mocked_url_fetcher->mock(request => sub { 'OK' });
my $real_object = RealObject->new;
$real_object = Test::MonkeyMock->new($real_object);
$real_object->mock(_build_url_fetcher => sub { $mocked_url_fetcher });
Таким образом, мы подменили фабричный метод, который создавал объект для получения файла по URL на свой тестовый.
Вместо фабричных методов, конечно, можно использовать и инъекцию зависимостей (если она поддерживается):
my $mocked_url_fetcher = Test::MockObject->new;
$mocked_url_fetcher->mock(request => sub { 'OK' });
my $real_object = RealObject->new(url_fetcher => $mocked_url_fetcher);
Перенос создания объектов в фабричные методы
В тестах, как и в коде, рекомендуется использовать фабрики или фабричные методы для создания тестируемых объектов. Это упрощает их инициализацию, избавляет от дублирования. Для тестов, которые используют наследование (например, на основе Test::Unit
или Test::Class
) эти методы позволяют подставлять нужные объекты в тесты. Например:
package TestBase;
use base 'Test::Unit::TestCase';
sub test_create_timer {
my $self = shift;
my $timer = $self->_build_timer;
$self->assert($timer);
}
sub test_increment_time {
my $self = shift;
my $timer = $self->_build_timer;
my $old_time = $timer->time;
$timer->tick;
my $new_time = $timer->time;
$self->assert($new_time > $old_time);
}
package OldTimerTest;
use base 'TestBase';
use OldTimer;
sub _build_timer {
my $self = shift;
return OldTimer->new;
}
package NewTimerTest;
use base 'TestBase';
use NewTimer;
sub test_some_new_functionality {
my $self = shift;
my $timer = $self->_build_timer;
$self->assert($timer->new_tick);
}
sub _build_timer {
my $self = shift;
return NewTimer->new;
}
Таким образом, тестируется и соответствие нового класса старому интерфейсу и новый функционал.
Перенос фикстур в отдельные классы
При тестировании большого проекта возникает необходимость создания типовых объектов практически в каждом тесте. Чтобы избежать дублирования, создаются специальные классы-фабрики для создания и инициализации этих объектов. Сама реализация этих фабрик может отличаться. Это может быть один класс для создания всех объектов или же разные классы для создания разных групп объектов. В английской терминологии это Mother или God Object и Test Data Builder.
Использование первого:
subtest 'table is round' => sub {
my $table = MotherObject->createTable();
ok $table->is_round;
};
Использование второго:
subtest 'table is round' => sub {
my $table = TableBuilder->create();
ok $table->is_round;
};
Простота важнее абстракций
Не всегда стоит стремиться полностью убрать дублирование из тестов. Излишняя абстракция делает их сложными для восприятия. Например, не стоит всю инициализацию объекта прятать в фабрику фикстур (см. предыдущий раздел). В самом тесте должно быть понятно, как создается объект, какие атрибуты ему передаются, что именно тестируется.
Юнит-тестирования недостаточно
Тестирование не останавливается на юнит-тестах. Юнит-тесты не могут протестировать систему целиком, нет уверенности, что классы правильно взаимодействуют между собой. В нашей практике мы используем функциональные тесты, которые тестируют систему как черный ящик. Безусловно, в некоторых случаях функциональные тесты тестируют практически тоже самое, что и юнит-тесты, но то, как они это тестируют, сильно отличается.
Кроме автоматического тестирования не стоит пренебрегать и ручным анализом кода, ручным тестированием особо важных частей системы на граничные случаи. В своей работе мы также используем сканеры безопасности.
Заключение
Это статья лишь бегло покрывает основы тестирования. Еще многое можно сказать о рекомендуемых модулях, организации тестов, mock-объектах, сложностях тестирования, как убедить коллег писать тесты, как убедить заказчика в необходимости рефакторинга и тестов. Пожалуйста, оставляйте свои комментарии, если и дальше хотите видеть в журнале статьи подобной тематики.
← От редактора | Содержание | Pjam — сервер сборки перловых приложений →