Как я могу использовать Test::LWP::UserAgent, если не могу напрямую заменить $ua в коде приложения?
У меня есть подпрограмма, которая получает некоторые данные из API через службу REST. Код довольно прост, но мне нужно отправить параметры в API, и мне нужно использовать SSL, поэтому я должен пройти через LWP::UserAgent и не могу использовать LWP:: Simple. Это упрощенная версия.
sub _request {
my ( $action, $params ) = @_;
# User Agent fuer Requests
my $ua = LWP::UserAgent->new;
$ua->ssl_opts( SSL_version => 'SSLv3' );
my $res = $ua->post(
$url{$params->{'_live'} ? 'live' : 'test'}, { action => $action, %$params }
);
if ( $res->is_success ) {
my $json = JSON->new;
return $json->decode( $res->decoded_content );
} else {
cluck $res->status_line;
return;
}
}
Это единственное место в моем модуле (которое не OOp), где мне нужно $ua
,
Теперь я хочу написать тест для этого, и после того, как некоторые исследования решили, что будет лучше использовать Test::LWP::UserAgent, который звучит очень многообещающе. К сожалению, есть подвох. В документе сказано:
Обратите внимание, что сам LWP::UserAgent не залатан обезьяной - вы должны использовать этот модуль (или подкласс) для отправки вашего запроса, иначе он не может быть перехвачен и обработан.
Один из распространенных механизмов замены реализации useragent - это лениво построенный атрибут Moose; если переопределение не предусмотрено во время построения, по умолчанию используется LWP::UserAgent-> new (% options).
Arghs. Очевидно, я не могу сделать вещь лося. Я не могу просто передать $ua
на саб, либо. Я мог бы, конечно, добавить дополнительный третий параметр $ua
на саб, но мне не нравится идея сделать это. Я чувствую, что не стоит так радикально изменять поведение такого простого кода, чтобы сделать его тестируемым.
Что я в основном хочу сделать, так это запустить мой тест следующим образом:
use strict;
use warnings;
use Test::LWP::UserAgent;
use Test::More;
require Foo;
Test::LWP::UserAgent->map_response( 'www.example.com',
HTTP::Response->new( 200, 'OK',
[ 'Content-Type' => 'text/plain' ],
'[ "Hello World" ]' ) );
is_deeply(
Foo::_request('https://www.example.com', { foo => 'bar' }),
[ 'Hello World' ],
'Test foo'
);
Есть ли способ, чтобы monkeypatch функциональность Test::LWP::UserAgent в LWP::UserAgent так, чтобы мой код просто использовал Test:: one?
3 ответа
Конечно, я мог бы добавить необязательный третий параметр $ ua к сабвуферу, но мне не нравится идея сделать это. Я чувствую, что не стоит так радикально изменять поведение такого простого кода, чтобы сделать его тестируемым.
Это известно как внедрение зависимостей, и это совершенно правильно. Для тестирования вы должны иметь возможность переопределять объекты, которые ваш класс будет использовать, чтобы высмеивать различные результаты.
Если вы предпочитаете более неявный способ переопределения объектов, рассмотрите Test::MockObject и Test:: MockModule. Вы могли бы посмеяться над конструктором LWP::UserAgent, чтобы вместо этого возвратить тестовый объект, или смоделировать более широкую часть кода, который вы тестируете, так что Test::LWP::UserAgent вообще не нужен.
Другой подход заключается в рефакторинге вашего производственного кода таким образом, чтобы компоненты (единицы) тестировались изолированно. Разделить HTTP-выборку от обработки ответа. Тогда очень просто протестировать вторую часть, создав собственный объект ответа и передав его внутрь.
В конечном итоге программисты используют все вышеперечисленные инструменты. Некоторые из них подходят для модульного тестирования, а другие - для более широкого интеграционного тестирования.
Измените свой код так, чтобы в _request()
звонишь _ua()
собрать свой пользовательский агент и переопределить этот метод в вашем тестовом скрипте. Вот так:
Внутри вашего модуля:
sub _request {
...
my $ua = _ua();
...
}
sub _ua {
return LWP::UserAgent->new();
}
В вашем тестовом скрипте:
...
Test::More::use_ok('Foo');
no warnings 'redefine';
*Foo::_ua = sub {
# return your fake user agent here
};
use warnings 'redefine';
... etc etc
На сегодняшний день я бы придерживался следующего подхода к этой проблеме. Представьте себе этот фрагмент унаследованного кода 1, который не является объектно-ориентированным и не может быть подвергнут рефакторингу, чтобы облегчить внедрение зависимостей.
package Foo;
use LWP::UserAgent;
sub frobnicate {
return LWP::UserAgent->new->get('http://example.org')->decoded_content;
}
Это действительно сложно проверить, и ответ RJH точен. Но в 2016 году у нас будет доступно несколько модулей больше, чем в 2013 году. Мне особенно нравится http://p3rl.org/Sub::Override, который заменяет подпрограмму в данном пространстве имен, но сохраняет ее только в текущей области видимости. Это отлично подходит для модульных тестов, потому что вам не нужно заботиться о восстановлении всего после того, как вы закончите.
package Test::Foo;
use strict;
use warnings 'all';
use HTTP::Response;
use Sub::Override;
use Test::LWP::UserAgent;
use Test::More;
# create a rigged UA
my $rigged_ua = Test::LWP::UserAgent->new;
$rigged_ua->map_response(
qr/\Qexample\E/ => HTTP::Response->new(
'200',
'OK',
[ 'Content-Type' => 'text/plain' ],
'foo',
),
);
# small scope for our override
{
# make LWP return it inside our code
my $sub = Sub::Override->new(
'LWP::UserAgent::new'=> sub { return $rigged_ua }
);
is Foo::frobnicate(), 'foo', 'returns foo';
}
Мы в основном создаем объект Test:: LWP:: UserAgent, который мы передаем всем нашим тестам. Мы также можем дать ему код ref, который будет запускать тесты по запросу, если мы хотим (здесь не показано). Затем мы используем Sub:: Override, чтобы конструктор LWP:: UserAgent не возвращал фактический LWP::UA, но уже подготовленный $rigged_ua
, Затем мы запускаем наш тест (ы). однажды $sub
выходит за рамки, LWP::UserAgent::new
восстанавливается и мы не мешаем ничему другому.
Важно всегда выполнять эти тесты с наименьшей возможной областью действия (как большинство вещей в Perl).
Если таких тестовых примеров много, хорошей стратегией будет создать какой-то хэш конфигурации для того, что вы ожидаете для каждого запроса, и использовать вспомогательную функцию построения для создания фальсифицированного пользовательского агента, а другую - для создания Sub:: Переопределить объект. Используемый в лексической области, этот подход очень мощный и в то же время довольно лаконичный.
1) представлены здесь отсутствием use strict
а также use warnings
,