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

Perl 如果无法直接替换应用程序代码中的$ua,如何使用Test::LWP::UserAgent?,perl,unit-testing,lwp,Perl,Unit Testing,Lwp,我有一个sub,它通过REST服务从API检索一些数据。代码相当简单,但我需要将参数发布到API,并且需要使用SSL,因此我必须通过,不能使用。这是它的简化版本 sub _request { my ( $action, $params ) = @_; # User Agent fuer Requests my $ua = LWP::UserAgent->new; $ua->ssl_opts( SSL_version => 'SSLv3' ); my $r

我有一个sub,它通过REST服务从API检索一些数据。代码相当简单,但我需要将参数发布到API,并且需要使用SSL,因此我必须通过,不能使用。这是它的简化版本

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
的地方

现在我想为此编写一个测试,在一些人决定最好使用它之后,这听起来很有希望。不幸的是,有一个陷阱。文件中说:

请注意,LWP::UserAgent本身不是猴子补丁的-您必须使用 此模块(或子类)无法发送您的请求,否则无法 捕获并处理

交换useragent实现的一种常见机制是通过 懒洋洋的驼鹿属性;如果此时未提供超驰控制 构造时间,默认为LWP::UserAgent->new(%options)

啊。显然我不能做驼鹿的事。我也不能把
$ua
传递给潜艇。当然,我可以向sub添加可选的第三个param
$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?

我当然可以向sub添加可选的第三个参数$ua,但我不喜欢这样做。我觉得仅仅为了使其可测试性而如此彻底地改变这种简单代码的行为是不好的

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

如果你喜欢一种更隐式的重写对象,考虑和。您可以模拟LWP::UserAgent的构造函数来返回测试对象,或者模拟正在测试的代码的更大部分,这样就根本不需要test::LWP::UserAgent

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


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

更改代码,以便在
\u request()
中调用
\u 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;
}
这确实很难测试,而且非常准确。但2016年,我们的可用模块比2013年多了一些。我特别喜欢,它替换给定名称空间中的子对象,但只在当前范围内保留它。这对于单元测试来说非常好,因为您不需要关心在完成之后恢复所有内容

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';
}
我们基本上创建一个对象,为所有测试用例提供数据。我们还可以给它一个代码引用,如果需要,它将对请求运行测试(此处未显示)。然后,我们使用Sub::Override使LWP::UserAgent的构造函数不返回实际的LWP::UA,而是返回已经准备好的
$rigged\u UA
。然后我们运行测试。一旦
$sub
超出范围,
LWP::UserAgent::new
就会恢复,我们不会干扰任何其他内容

务必以尽可能小的范围进行这些测试(就像Perl中的大多数事情一样)

如果有很多这样的测试用例,那么为每个请求构建某种类型的配置哈希,并使用building helper函数创建被操纵的用户代理,以及使用另一个函数创建Sub::Override对象,这是一个很好的策略。在词汇范围内使用,这种方法非常强大,同时也非常简洁



1) 在这里,IMO缺乏
使用严格的
使用警告

这两个方面,可测试性是一个有效的代码问题。编码的目标是让某些东西“起作用”。所以,目标状态的陈述应该是这样的:“它起作用了”。然而,经验告诉我:“在测试表明它有效之前,任何东西都不‘有效’。”谢谢你的建议。当我读到T::LWP::UA文档在中所说的(CPAN上的大多数模拟库都使用Test::MockObject,这被广泛认为是不好的实践…)时,我有点忘记了这两个测试。我经常使用MockModule,但不知怎么的,我在这里完全忘记了这一点。Test::LWP::UserAgent所做的一切听起来很好,可以让你自己去摆弄自己谢谢你的建议。不过,我更喜欢@rjh的方法。我不想改变太多。