How can I use Test :: LWP :: UserAgent if I cannot directly replace $ ua in my application code?

I have a sub that retrieves some data from an API via a REST service. The code is pretty simple, but I need to publish the API parameters and I need to use SSL, so I need to go through LWP :: UserAgent and cannot use LWP :: Simple . This is a simplified version.

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 it $ua

.

Now I want to write a test for this and after some research , it was decided to use Test :: LWP :: UserAgent , which sounds really promising. Unfortunately there is a catch. The doc says:

Please note that LWP :: UserAgent itself is not patched - you must use this module (or subclass) to send your request, or it may not be caught and processed.

One common mechanism for overriding the useragent implementation is through the lazily constructed Moose attribute; if no override is provided at construction time, the default for LWP :: UserAgent-> new (% options).

Arghs. Obviously, I cannot make a moose. I can't just pass $ua

to sub. I could of course add an additional sub param $ua

to the sub, but I don't like the idea of ​​doing that. I feel it's not a good idea to change the behavior of such simple code radically enough to make it testable.

Basically I want to execute 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 disarm the Test :: LWP :: UserAgent function in LWP :: UserAgent so that my code would just use Test :: one?

+3


source to share


3 answers


I could of course add an additional sub param $ ua to the sub, but I don't like the idea of ​​doing that. I feel it's not a good idea to change the behavior of such simple code radically enough to make it testable.

This is called dependency injection and it is perfectly true. For testing, you need to override the objects that your class will use to simulate different results.

If you prefer a more implicit way of overriding objects, consider Test :: MockObject and Test :: MockModule . You could mock the LWP :: UserAgent constructor to return a test object instead, or mock a wider part of the code you are testing so that Test :: LWP :: UserAgent is not needed at all.



Another approach is to refactor your production code so that the (single) components are tested in isolation. Split HTTP fetch with response processing. It is then very easy 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 suitable for unit testing and others for broader integration testing.

+2


source


Change your code so that _request()

you are calling _ua()

to build your user agent and override this method in your test script. For example:

Inside your module:

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

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

      



Within a test script:

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

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

      

+4


source


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 to facilitate dependency injection.

package Foo;
use LWP::UserAgent;

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

      

This is really hard to check and rjh's answer is in place. But in 2016 we have a few more modules than in 2013. I especially like Sub :: Override , which replaces the sub in the given namespace, but only keeps it in the current scope. This is great for unit tests because you don't have to worry 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 pass to all our test cases. We can also give it a ref code that will run tests on demand if we want (not shown here). We then use Sub :: Override to prevent LWP :: UserAgent from returning the actual LWP :: UA, but already prepared $rigged_ua

. We then run our test (s). As soon as it $sub

goes out of scope, it is LWP::UserAgent::new

restored, and we do not interfere with anything.

It's important to always run these tests in the smallest possible scope (like most things in Perl).

If there are many such tests, it is a good strategy to create your own config hash for what you expect from each request and use the building helper to create a spoofed user agent and another to create a Sub :: Override object. Used in the lexical domain, this approach is very powerful and rather concise at the same time.


1), represented here by the absence of use strict

and use warnings

.

0


source







All Articles