Search code examples
perlunit-testinglwp

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. 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. 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.

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. 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.

One common mechanism to swap out the useragent implementation is via a lazily-built Moose attribute; if no override is provided at construction time, default to LWP::UserAgent->new(%options).

Arghs. Obviously I cannot do the Moose thing. I can't just pass a $ua to the sub, either. I could of course add an optional third param $ua to the sub, but I don't like the idea of doing that. 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?


Solution

  • I could of course add an optional third param $ua to the sub, but I don't like the idea of doing that. 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. 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.

    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. 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.