Как я могу использовать 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 ,

Другие вопросы по тегам