Тестирование модуля Perl с помощью теста:: Подробнее (промежуточный Perl, глава 14)
Это мой первый вопрос для. Извините заранее, если я нарушу некоторые правила.
Я читал главу 14 Intermediate Perl, 2-е изд., в котором обсуждается тестирование модулей Perl и использование функций из Test:: More. Я имею в виду код, опубликованный непосредственно в этой книге в разделе "Добавление наших первых тестов".
Для некоторого фона в этой главе образец Animal
создается в модуле с тем же именем. Этот класс имеет простой метод speak
, который выглядит следующим образом:
sub speak {
my $class = shift;
print "a $class goes ", $class->sound, "!\n";
}
Метод sound
- это простая строка, возвращаемая для определенного Animal, поэтому, например, метод Horse sound
будет просто sub sound { "neigh" }
, а метод speak
должен выводить следующее:
A Horse goes neigh!
Проблема, с которой я сталкиваюсь, заключается в следующем: в тестовом коде, который я создал в. /Animal/t/Animal.t, мне поручено использовать только изолированные блоки и Test::More::is
, чтобы проверить, что speak
работает. Код выглядит так в тестовом файле:
[test code snip]
{
package Foofle;
use parent qw(Animal);
sub sound { 'foof' }
is( Foofle->speak,
"A Foofle goes foof!\n",
"An Animal subclass does the right thing"
);
}
Тест не проходит. Я запускал все команды Build, но при запуске "Build test" я получаю этот сбой для теста Animal:
Undefined subroutine &Foofle::is called at t/Animal.t line 28.
Когда я пытаюсь явно использовать Test::More::is
вместо простого is
, тест по-прежнему не работает со следующим сообщением:
# Failed test 'An Animal subclass does the right thing'
# at t/Animal.t line 28.
# got: '1'
# expected: 'A Foofle goes foof!
# '
Мои методы отображаются точно так, как я объяснил. Я думаю, что первая ошибка - проблема с областью из-за голых блоков, но не на 100%. Вторая ошибка, о которой я не знаю, потому что, если бы я должен был создать класс Foofle
как дочерний элемент Animal
и называть его speak
, я бы не получил 1 ответ, а скорее ожидаемый результат.
Кто-нибудь сможет помочь мне в том, что я могу сделать неправильно? Для возможных версий программного обеспечения я использую perl v5.16, Test:: More v0.98 и Module:: Starter v1.58.
Ответы
Ответ 1
Вы правильно объяснили причину первой ошибки и исправили ее правильно (с указанием правильного имени пакета). Но вы, кажется, пропустили простой факт: speak
метод класса Animal не return
этот a $class goes...
string - он возвращает результат его печати (который равен 1
)!
Смотрите, эта подпрограмма:
sub speak {
my $class = shift;
print "a $class goes ", $class->sound, "!\n";
}
... не имеет явного выражения return
. В этом случае return - результат вычисления последней вызываемой инструкции подпрограммы - результат вычисления print something
, который равен 1
(true
, фактически).
То почему испытание терпит неудачу. Вы можете исправить это либо с помощью теста для 1
(но это слишком тривиально, я полагаю), либо изменив сам метод, чтобы он вернул строку, которую он печатает. Например:
sub speak {
my $class = shift;
my $statement = "a $class goes " . $class->sound . "!\n";
print $statement;
return $statement;
}
... и, откровенно говоря, оба подхода выглядят немного... рыбно. Последний, хотя и явно более полный, фактически не будет охватывать все функции этого метода speak
: он проверяет, было ли утверждение правильным или не только, но не было ли оно напечатано или нет. )
Ответ 2
Вы уже поняли, что проблема с вызовом is
заключалась в том, что вы были в неправильном пакете в момент совершения вызова. Полностью указывая имя функции, как вы это делали, а также импортирует is
в ваше пространство имен, говоря
use Test::More;
где-то в пакете с тестом.
Ответ на остальную часть вашего вопроса заключается в различии между тем, что вы тестируете, и тем, что вы делаете. Что делает speak
, но при запросе is(speak, ...)
вы спрашиваете, что возвращает speak
, что не связано с тем, что оно напечатало. Это действительно не очень полезное возвращаемое значение print
.
Так как целью speak
является печать определенной строки, тест для speak
должен проверить, что он действительно напечатал строку и что она была правильной строкой. Для теста, чтобы сделать это, вы должны каким-то образом захватить то, что было напечатано.
На самом деле существует несколько способов сделать это, используя IO::File
, чтобы принудительно указать дескриптор файла для печати для обезьяны, исправляя замену для print
в ваш класс, но следующий метод doesn 't требует каких-либо изменений в тестируемой системе, чтобы улучшить ее тестируемость.
Встроенный select
позволяет вам изменять, где печатается print
. Выходной канал по умолчанию STDOUT
, хотя вы обычно должны притворяться, что не знаете этого. К счастью, вы также можете использовать select
для обнаружения исходного дескриптора файла, хотя, вероятно, вы должны убедиться, что вы восстановили дескриптор файла по умолчанию (который, в конце концов, глобальная переменная), даже если ваш тест по какой-то причине умирает. Поэтому вам нужно управлять исключениями. И вам нужен дескриптор файла, который вы можете проверить содержимое и не обязательно на самом деле распечатать что-либо; IO::Scalar
может помочь там.
При таком подходе вы можете проверить исходный код с помощью
package AnimalTest;
use IO::Scalar;
use Test::More tests => 1;
use Try::Tiny;
{
package Foofle;
use base qw(Animal);
sub sound { 'foof' }
}
{
my $original_FH = select;
try {
my $result;
select IO::Scalar->new(\$result);
Foofle->speak();
is(
$result, "A Foofle goes foof!\n",
"An Animal subclass does the right thing"
);
} catch {
die $_;
} finally {
select $original_FH;
};
}
Try::Tiny
следит за тем, чтобы вы не мутировали, если speak
приводит к аневризме Animal
, print
перенаправляется для изменения скаляра, а не фактической печати на экране, и теперь тест не выполняется по правильной причине, а именно: строки имеют несогласованную капитализацию.
Вы заметите, что в нем много работы; это связано с тем, что тестируемая система не особенно хорошо настроена для проверки, и поэтому мы должны компенсировать ее. В моем собственном коде это не тот подход, который я бы выбрал, вместо этого я предпочел бы сделать исходный код более пригодным для тестирования. Затем для тестирования я monkey-patch (т.е. Переопределяет один из тестируемых методов), часто используя TMOE. Этот подход выглядит более как это:
[in Animal:]
sub speak {
my $class = shift;
$class->print("a $class goes ", $class->sound, "!\n");
}
sub print {
my $class = shift;
print @_;
}
[позже:]
{
package Foofle;
use base qw(Animal);
sub sound { 'foof' }
sub print {
my ($self, @text) = @_;
return join '', @text;
}
}
is(
Foofle->speak(), "A Foofle goes foof!\n",
"An Animal subclass does the right thing"
);
Вы заметите, что это намного больше похоже на ваш исходный код. Основное отличие состоит в том, что вместо прямого вызова встроенного print
, Animal
вызывает $class->print
, который, в свою очередь, вызывает встроенный print
. Подкласс Foofle
затем переопределяет метод print
, чтобы возвращать свои аргументы, а не печатать их, что дает тестовому коду доступ к тому, что было бы напечатано.
Этот подход намного чище, чем необходимость изменять глобальные переменные, чтобы выяснить, что печатается, но имеет два недостатка: он требует модификации тестируемого кода, чтобы сделать его более проверяемым, и он никогда не проверяет, печатает ли случается. Он просто проверяет, что print
вызывается с правильными аргументами. Поэтому крайне важно, чтобы Animal:: print был настолько тривиальным, чтобы быть явно правильным путем проверки.
Ответ 3
Я представляю, что ваш код выглядит примерно так:
package SomeTest; # if omitted, it like saying "package main"
use Test::More;
...
{
package Foofle;
is( something, something_else );
}
Оператор use Test::More
экспортирует некоторые из функций Test::More
в вызывающее пространство имен, в этом случае SomeTest
(или main
). Это означает, что функции будут определены для символов main::is
, main::ok
, main::done_testing
и т.д.
В блоке, который начинается с package Foofle
, вы теперь находитесь в пространстве имен Foofle
, поэтому теперь Perl будет искать функцию, соответствующую символу Foofle::is
. Он не найдет его, поэтому он будет жаловаться и выходить.
Обходной путь заключается в том, чтобы импортировать Test::More
в пространство имен Foofle
.
{
package Foofle;
use Test::More;
is( something, something_else );
}
а другой - использовать полное имя метода для вызова is
:
{
package Foofle;
Test::More::is( something, something_else );
}