如果我不能直接替换应用程序代码中的$ ua,我如何使用Test :: LWP :: UserAgent?

时间:2013-02-19 14:55:45

标签: perl unit-testing lwp

我有一个sub,它通过REST服务从API检索一些数据。代码相当简单,但我需要将参数发布到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

现在我想为此编写一个测试,经过一些research决定最好使用Test::LWP::UserAgent,这听起来真的很有意义。不幸的是,有一个问题。在文档中,它说:

  

请注意,LWP :: UserAgent本身不是猴子修补程序 - 您必须使用   这个模块(或子类)发送您的请求,或者它不能   抓住并处理了。

     

交换使用者实施的一种常见机制是通过a   懒惰的穆斯属性;如果没有提供覆盖   施工时间,默认为LWP :: UserAgent-> new(%options)。

Arghs。显然我不能做麋鹿的事情。我也不能只将$ua传递给子。我当然可以在sub中添加一个可选的第三个参数$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'
);

有没有办法将Test :: LWP :: UserAgent功能monkeypatch到LWP :: UserAgent,以便我的代码只使用Test :: one?

3 个答案:

答案 0 :(得分:4)

更改您的代码,以便在_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 :(得分:2)

  

我当然可以在sub中添加一个可选的第三个param $ ua,但我不喜欢这样做的想法。我觉得改变这种简单代码的行为是不可行的,只是为了让它可以测试。

这称为依赖注入,它完全有效。对于测试,您需要能够覆盖您的类将用于模拟各种结果的对象。

如果您更喜欢更隐式的覆盖对象方式,请考虑Test::MockObjectTest::MockModule。您可以模拟LWP :: UserAgent的构造函数来返回测试对象,或者模拟您正在测试的代码的更大部分,以便根本不需要Test :: LWP :: UserAgent。

另一种方法是重构您的生产代码,使组件可以(单元)单独测试。从处理响应中分离HTTP提取。然后通过创建自己的响应对象并将其传入来测试第二部分非常简单。

最终程序员使用上述所有工具。有些适用于单元测试,有些则适用于更广泛的集成测试。

答案 2 :(得分:0)

截至今天,我将采用以下方法解决此问题。想象一下这段遗留代码 1 ,它不是面向对象的,不能重构,因此它可以简化依赖注入。

package Foo;
use LWP::UserAgent;

sub frobnicate {
    return LWP::UserAgent->new->get('http://example.org')->decoded_content;
}

测试确实很棘手,rjh's answer是正确的。但是在2016年,我们可以获得比2013年更多的模块。我特别喜欢Sub::Override,它取代了给定命名空间中的sub,但只保留在当前范围内。这使得它非常适合单元测试,因为在完成后你不需要关心恢复所有内容。

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对象,我们可以提供所有测试用例。我们还可以给它一个代码引用,如果我们想要的话,将在请求上运行测试(这里没有显示)。然后我们使用Sub :: Override使LWP :: UserAgent的构造函数不返回实际的LWP :: UA,而是已经准备好的$rigged_ua。然后我们运行我们的测试。 $sub超出范围后,LWP::UserAgent::new会恢复,我们不会干扰任何其他内容。

始终以尽可能小的范围进行测试非常重要(就像Perl中的大多数事情一样)。

如果有很多测试用例,那么为每个请求构建某种配置哈希是一个很好的策略,并使用构建帮助函数来创建被装配的用户代理,另一个一个创建Sub :: Override对象。在词法范围中使用,这种方法非常强大,同时非常简洁。

1)此处由缺少use strictuse warnings表示。