简体   繁体   English

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

[英]How can I use Test::LWP::UserAgent if I cannot replace the $ua in the app code directly?

I've got a sub that retrieves some data from an API via a REST service. 我有一个sub通过REST服务从API检索一些数据。 The code is rather simple, but I need to post parameters to the API and I need to use SSL, so I have to go through LWP::UserAgent and cannot use LWP::Simple . 代码相当简单,但我需要将参数发布到API,我需要使用SSL,所以我必须通过LWP :: UserAgent并且不能使用LWP :: Simple This is a simplified version of it. 这是它的简化版本。

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;
  }
}

This is the only place in my module (which is not OOp) where I need the $ua . 这是我模块中唯一的地方(不是OOp),我需要$ua

Now I want to write a test for this and after some research decided it would be best to use Test::LWP::UserAgent , which sounds really promissing. 现在我想为此编写一个测试,经过一些研究决定最好使用Test :: LWP :: UserAgent ,这听起来真的很有意思。 Unfortunately, there's a catch. 不幸的是,有一个问题。 In the doc, it says: 在文档中,它说:

Note that LWP::UserAgent itself is not monkey-patched - you must use this module (or a subclass) to send your request, or it cannot be caught and processed. 请注意,LWP :: UserAgent本身不是猴子修补程序 - 您必须使用此模块(或子类)发送请求,否则无法捕获和处理它。

One common mechanism to swap out the useragent implementation is via a lazily-built Moose attribute; 换出useragent实现的一种常见机制是通过一个延迟构建的Moose属性; if no override is provided at construction time, default to LWP::UserAgent->new(%options). 如果在构造时没有提供覆盖,则默认为LWP :: UserAgent-> new(%options)。

Arghs. Arghs。 Obviously I cannot do the Moose thing. 显然我不能做麋鹿的事情。 I can't just pass a $ua to the sub, either. 我也不能将$ua传递给sub。 I could of course add an optional third param $ua to the sub, but I don't like the idea of doing that. 我当然可以在sub中添加一个可选的第三个param $ua ,但我不喜欢这样做的想法。 I feel it's not ok to alter the behaviour of such simple code so radically just to make it testable. 我觉得改变这种简单代码的行为是不可能的,只是为了让它可以测试。

What I basically want to do is run my test like this: 我基本上想要做的是像这样运行我的测试:

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'
);

Is there a way to monkeypatch the Test::LWP::UserAgent functionality into LWP::UserAgent so that my code just uses the Test:: one? 有没有办法将Test :: LWP :: UserAgent功能monkeypatch到LWP :: UserAgent,以便我的代码只使用Test :: one?

Change your code so that within _request() , you're calling _ua() to collect your user agent and override this method in your test script. 更改您的代码,以便在_request() ,您调用_ua()来收集您的用户代理并在您的测试脚本中覆盖此方法。 Like so: 像这样:

Within your module: 在你的模块中:

sub _request {
...
 my $ua = _ua();
...
}

sub _ua { 
 return LWP::UserAgent->new();
}

Within your test script: 在您的测试脚本中:

...
Test::More::use_ok('Foo');

no warnings 'redefine';
*Foo::_ua = sub { 
    # return your fake user agent here
};
use warnings 'redefine';
... etc etc

I could of course add an optional third param $ua to the sub, but I don't like the idea of doing that. 我当然可以在sub中添加一个可选的第三个param $ ua,但我不喜欢这样做的想法。 I feel it's not ok to alter the behaviour of such simple code so radically just to make it testable. 我觉得改变这种简单代码的行为是不可能的,只是为了让它可以测试。

This is known as dependency injection and it's perfectly valid. 这称为依赖注入,它完全有效。 For testing you need to be able to override objects your class will use to mock various results. 对于测试,您需要能够覆盖您的类将用于模拟各种结果的对象。

If you prefer a more implicit way of overriding objects, consider Test::MockObject and Test::MockModule . 如果您更喜欢覆盖对象的更隐式方法,请考虑Test :: MockObjectTest :: MockModule You could mock LWP::UserAgent's constructor to return a test object instead, or mock a wider part of the code you are testing such that Test::LWP::UserAgent is not needed at all. 您可以模拟LWP :: UserAgent的构造函数来返回测试对象,或者模拟您正在测试的代码的更大部分,以便根本不需要Test :: LWP :: UserAgent。

Another approach is to refactor your production code such that the components are (unit) testable in isolation. 另一种方法是重构您的生产代码,使组件(单元)可以单独测试。 Split the HTTP fetching from the processing of the response. 从处理响应中分离HTTP提取。 Then it is very simple to test the second part, by creating your own response object and passing it in. 然后通过创建自己的响应对象并将其传入,测试第二部分非常简单。

Ultimately programmers use all of the above tools. 最终程序员使用上述所有工具。 Some are appropriate for unit testing and others for wider integration testing. 一些适用于单元测试,另一些适用于更广泛的集成测试。

As of today, I would go with the following approach to this problem. 截至今天,我将采用以下方法来解决这个问题。 Imagine this piece of legacy code 1 , that is not object oriented and cannot be refactored so that it makes dependency injection easy. 想象一下这段遗留代码1 ,它不是面向对象的,不能重构,因此它使依赖注入变得容易。

package Foo;
use LWP::UserAgent;

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

This is indeed tricky to test, and rjh's answer is spot-on. 这测试确实很棘手,而且rjh的答案正确的 But in 2016 we have a few more modules available than we did back in 2013. I particularly like Sub::Override , which replaces a sub in a given namespace, but only keeps it around in the current scope. 但是在2016年,我们可以获得比2013年更多的模块。我特别喜欢Sub :: Override ,它取代了给定命名空间中的sub,但只保留在当前范围内。 That makes it great for unit tests, because you don't need to care about restoring everything after you're done. 这使它非常适合单元测试,因为在完成后你不需要关心恢复所有内容。

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';
}

We basically create a Test::LWP::UserAgent object that we feed all of our test cases. 我们基本上创建一个Test :: LWP :: UserAgent对象,我们提供所有测试用例。 We can also give it a code ref that will run tests on the request if we want (not shown here). 我们还可以给它一个代码引用,如果我们想要的话,将在请求上运行测试(这里没有显示)。 We then use Sub::Override to make LWP::UserAgent's constructor not return an actual LWP::UA, but the already prepared $rigged_ua . 然后我们使用Sub :: Override使LWP :: UserAgent的构造函数不返回实际的LWP :: UA,而是已经准备好的$rigged_ua Then we run our test(s). 然后我们运行我们的测试。 Once $sub goes out of scope, LWP::UserAgent::new gets restored and we do not interfere with anything else. 一旦$sub超出范围, LWP::UserAgent::new就会恢复,我们不会干扰其他任何事情。

It's important to always do those tests with the smallest possible scope (as most things in Perl). 始终以尽可能小的范围进行这些测试非常重要(就像Perl中的大多数事情一样)。

If there are a lot of those test cases, it's a good strategy to build some kind of configuration hash for what you expect for each request, and use a building helper function to create the rigged user agent, and another one to create the Sub::Override object. 如果有很多测试用例,那么为每个请求构建某种配置哈希是一个很好的策略,并使用构建助手函数来创建被装配的用户代理,另一个用于创建Sub: :覆盖对象。 Used in a lexical scope, this approach is very powerful and pretty concise at the same time. 在词法范围中使用,这种方法非常强大,同时非常简洁。


1) represented here by the lack of use strict and use warnings . 1)这里表示缺乏use strictuse warnings

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM