Тестирование модуля 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 );
}