A piggy bank of commands, fixes, succinct reviews, some mini articles and technical opinions from a (mostly) Perl developer.

Jump to

Quick reference

Showing posts with label mock. Show all posts
Showing posts with label mock. Show all posts

Recording HTTP responses with Perl

There are two main ways. If your HTTP requests are being done in a different way, you will need to build your own solution.

The LWP way:

use LWP::UserAgent::Mockable;

BEGIN {
    # Options: record|playback|passthrough
    $ENV{LWP_UA_MOCK}      ||= 'playback';
    $ENV{LWP_UA_MOCK_FILE} ||= "$0-lwp-mock.out";
}

END {
    LWP::UserAgent::Mockable->finished();
}

# You might be able to normalise the requests using the callbacks

The Mojo way:

use Mojo::UserAgent::Mockable;

my $mock_ua = Mojo::UserAgent::Mockable->new(
    mode        => $ENV{MOJO_UA_MOCK},
    file        => "$0-mojo-ua-mock.out",
#    ignore_body => 1,
# or:
    request_normalizer => sub {
        my ( $req, $recorded_req ) = @_; 

        my ($time) = $recorded_req->body =~ m{};
        my ($hash) = $recorded_req->body =~ m{(.*)};

        my $body = $req->body;
        $body =~ s{}{};
        $body =~ s{.*}{$hash};

        $req->body($body);
    },  

);

END {
    $mock_ua->save if $ENV{MOJO_UA_MOCK} eq 'record';
}

ok my $t = Test::Mojo->new("My::App");

$t->app->mock( useragent => sub { $mock_ua } );

# Configure the proxy in record/passthrough mode only
if ( $MODE =~ /record|passthrough/ ) { 
    local $ENV{HTTPS_PROXY} = $t->app->config->{http_proxy};
    local $ENV{HTTP_PROXY}  = $t->app->config->{http_proxy};
    my $proxy = Mojo::UserAgent::Proxy->new;
    $proxy->detect;
    $mock_ua->proxy( $proxy );
}

Mock time with Test::Time for Perl

Voila:

use Test::Time time => 1;

my $now = time;    # $now is equal to 1
sleep 300;         # returns immediately, displaying a note
my $then = time;   # $then equals to 301

Mock modules & Rules of mocking for Perl tests

Mock modules

  • Test2::V0 - I really must start using this soon. See Test2::Mock and possibly Test2::Tools::Mock
  • Test::MockObject - Works. Has set_isa() to pass Moose constraints. Start with an empty object and add methods as needed. This one.
    • see also Test::MockObject::Extends 
  • Test::MockModule - "Override subroutines in a module for unit testing".
    • Does not fool Moose (modules appear as Test::MockModule instead of what they are).
    • Only overrides the methods you say.
    • Has strict mode (can't accidentally mock non-existent methods)
    • Has an 'original()' function for wrapping methods (source):
      • my $mock = Test::MockModule->new("MyModule");
        $mock->redefine("something", sub { my ($self, $args) = @_; push @record_for_testing_later, $args;
                return $mock->original(@_);
        });
  • DBD::Mock - don't use this, use Test::DBIx::Class (with DBIx::Class::Fixtures) or a real test database.
  • Test::Mock::Class - looks clunky
  • Mock::Quick - another one, syntax may be better. "less side effects", doesn't reload modules.
    • To call original un-mocked method, use (source):
    • my $mock = qtakeover 'Some::Module' => (
          id => sub {
              my ( $mock_self, @args ) = @_;
              $counter->{id}++ if scalar @args;
              return $mock_self->MQ_CONTROL->original('id')->( $mock_self, @args );
          },
      );

Reasons to use Mock::Quick

Monkey patching modules

  • Manually overriding subroutines AKA monkey patching - risky. What if you miss a method? Same as what Test::MockModule does.
    • example: { package X; *method_name = sub { return 'foo'; } }
  • Class::Monkey
  • Monkey::Patch
  • Mojo::Util::monkey_patch
  • Sub::Override
  • Mock::Sub - another one
  • Mock::MonkeyPatch
    • Can easily call original sub with Mock::MonkeyPatch::ORIGINAL

Rules of mocking

Don't monkey patch. Don't modify the symbol table directly like this:
*Acme::Foo::method = sub { return "mock you"; }

Instead use Test::MockModule or Sub::Override or similar. Reasons:
  • If you type the method name wrong, or if it gets moved, it becomes so obvious that it can't be missed
  • You won't continue to test code that doesn't exist anymore
The first rule of mocking is: Don't mock.

Instead use dependency injection. If you think you want to use a mock:
  • instead expose the object as an attribute,
  • and pass in one mock object once
  • this is easier to develop & maintain than mocking different things in every test
  • and less prone to leakage between tests
Or if you need a mock method to be in place when an app is instantiated, add logic in the attribute to load a custom object from config, or default to the real object if nothing found in config:
  • create a test class like Foo::Test that inherits from Foo
  • override the dangerous methods
  • add test code: my $test_foo = Foo::Test->(@test_args)
  • and then: $t = Test::Mojo->new("App", { foo => $test_foo });
  • instantiate like this: $class = $config->foo || Foo->new(@args)
  • if necessary, the main library and test library should share an abstract interface (implemented via a role)
Only if all the above fail, use MockModule. Note that scope can matter when using set_isa()