The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Mojo::UserAgent::Mockable - A Mojo User-Agent that can record and play back requests without Internet connectivity, similar to LWP::UserAgent::Mockable

VERSION

version 1.11

SYNOPSIS

    my $ua = Mojo::UserAgent::Mockable->new( mode => 'record', file => '/path/to/file' );
    my $tx = $ua->get($url);

    # Then later...
    my $ua = Mojo::UserAgent::Mockable->new( mode => 'playback', file => '/path/to/file' );
    
    my $tx = $ua->get($url); 
    # This is the same content as above. The saved response is returned, and no HTTP request is
    # sent to the remote host.
    my $reconstituted_content = $tx->res->body;

ATTRIBUTES

mode

Mode to operate in. One of:

passthrough

Operates like Mojo::UserAgent in all respects. No recording or playback happen.

record

Records all transactions made with this instance to the file specified by "file".

playback

Plays back transactions recorded in the file specified by "file"

lwp-ua-mockable

Works like LWP::UserAgent::Mockable. Set the LWP_UA_MOCK environment variable to 'playback', 'record', or 'passthrough', and the LWP_UA_MOCK_FILE environment variable to the recording file.

file

File to record to / play back from.a

unrecognized

What to do on an unexpected request. One of:

exception

Throw an exception (i.e. die).

null

Return a response with empty content

fallback

Process the request as if this instance were in "passthrough" mode and perform the HTTP request normally.

ignore_headers

Request header names to ignore when comparing a request made with this class to a stored request in playback mode. Specify 'all' to remove any headers from consideration. By default, the 'Connection', 'Host', 'Content-Length', and 'User-Agent' headers are ignored.

METHODS

save

In record mode, save the transaction cache to the file specified by "file" for later playback.

THEORY OF OPERATION

Recording mode

For the life of a given instance of this class, all transactions made using that instance will be serialized and stored in memory. When the instance goes out of scope, or at any time "save" is called, the transaction cache will be written to the file specfied by "file" in JSON format. Transactions are stored in the cache in the order they were made.

Playback mode

When this class is instantiated, the instance will read the transaction cache from the file specified by "file". When a request is first made using the instance, if the request matches that of the first transaction in the cache, the request URL will be rewritten to that of the local host, and the response from the first stored transaction will be returned to the caller. Each subsequent request will be handled similarly, and requests must be made in the same order as they were originally made, i.e. if orignally the request order was A, B, C, with responses A', B', C', requests in order A, C, B will NOT return responses A', C', B'. Request A will correctly return response A', but request C will trigger an error (behavior configurable by the "unrecognized" option).

Request matching

Two requests are considered to be equivalent if they have the same URL (order of query parameters notwithstanding), the same body content, and the same headers. You may exclude headers from consideration by means of the "ignore_headers" attribute.

CAVEATS

Encryption

The playback file generated by this module is unencrypted JSON. Treat the playback file as if its contents were being transmitted over an unsecured channel.

Local application server

Using this module against a local app, e.g.:

    my $app = Mojolicious->new;
    ...

    my $ua = Mojo::UserAgent::Mockable->new;
    $ua->server->app($app);

Doesn't work, because in playback mode, requests are served from an internal Mojolicious instance. So if you blow that away, the thing stops working, natch. You should instead instantiate Mojo::Server::Daemon and connect to the app via the server's URL, like so:

    use Mojo::Server::Daemon;
    use Mojo::IOLoop;

    my $app = Mojolicious->new;
    $app->routes->any( ... );

    my $daemon = Mojo::Server::Daemon->new(
        app => $app, 
        ioloop => Mojo::IOLoop->singleton,
        silent => 1,
    );
    
    my $listen = q{http://127.0.0.1};
    $daemon->listen( [$listen] )->start;
    my $port = Mojo::IOLoop->acceptor( $daemon->acceptors->[0] )->port;
    my $url  = Mojo::URL->new(qq{$listen:$port})->userinfo('joeblow:foobar');
    
    my $output_file = qq{/path/to/file.json};
    
    my $mock = Mojo::UserAgent::Mockable->new(ioloop => Mojo::IOLoop->singleton, mode => 'record', file => $output_file);
    my $tx = $mock->get($url);

Mojolicious::Lite

You will often see tests written using Mojolicious::Lite like so:

    use Mojolicious::Lite;

    get '/' => sub { ... };

    post '/foo' => sub { ... };

And then, further down:

    my $ua = Mojo::UserAgent->new;

    is( $ua->get('/')->res->text, ..., 'Text OK' );
Or:

    use Test::Mojo;
    my $t = Test::Mojo->new;
    $t->get_ok('/')->status_is(200)->text_is( ... );

And this is all fine. Where it stops being fine is when you have Mojo::UserAgent::Mockable on board:

    use Mojolicious::Lite;

    get '/' => sub { ... };

    post '/foo' => sub { ... };
    
    use Test::Mojo;
    my $t = Test::Mojo->new;
    my $mock = Mojo::UserAgent::Mockable->new( mode => 'playback', file => ... );
    $t->get_ok('/')->status_is(200)->text_is( ... );

Mojolicious::Lite will replace the current UA's internal application server's application instance ("app" in Mojo::UserAgent::Server) with the Mojolicious::Lite application. This will break the playback functionality, as this depends on a custom Mojolicious application internal to the module. Instead, define your application in a separate package (not necessarily a separate file), like so:

    package MyApp;
    use Mojolicious::Lite;
    get '/' => sub { ... };
    post '/foo' => sub { ... };

    # Actual test application
    package main;

    use Mojo::UserAgent::Mockable;
    use Mojo::Server::Daemon;
    use Mojo::IOLoop;
    use Test::Mojo;

    $app->routes->get('/' => sub { ... });
    $app->routes->post('/foo' => sub { ... });

    my $daemon = Mojo::Server::Daemon->new(
        app    => $app,
        ioloop => Mojo::IOLoop->singleton,
        silent => 1,
    );

    my $listen = q{http://127.0.0.1};
    $daemon->listen( [$listen] )->start;
    my $port = Mojo::IOLoop->acceptor( $daemon->acceptors->[0] )->port;
    my $url  = Mojo::URL->new(qq{$listen:$port})->userinfo('joeblow:foobar');

    my $mock = Mojo::UserAgent::Mockable->new(ioloop => Mojo::IOLoop::singleton, mode => playback, file => ... );
    my $t = Test::Mojo->new;
    $t->ua($mock);
    $mock->get_ok($url->clone->path('/'))->status_is(200)->text_is( ... );

You can also do the following (as seen in t/030_basic_authentication.t):

    use Mojolicious;
    use Mojo::Server::Daemon;
    use Mojo::IOLoop;

    my $app = Mojolicious->new;
    $app->routes->get('/' => sub { ... });
    $app->routes->post('/foo' => sub { ... });

    my $daemon = Mojo::Server::Daemon->new(
        app    => $app,
        ioloop => Mojo::IOLoop->singleton,
        silent => 1,
    );

    my $listen = q{http://127.0.0.1};
    $daemon->listen( [$listen] )->start;
    my $port = Mojo::IOLoop->acceptor( $daemon->acceptors->[0] )->port;
    my $url  = Mojo::URL->new(qq{$listen:$port})->userinfo('joeblow:foobar');

    my $mock = Mojo::UserAgent::Mockable->new(ioloop => Mojo::IOLoop::singleton, mode => playback, file => ... );
    my $t = Test::Mojo->new;
    $t->ua($mock);
    $t->get_ok('/')->status_is(200)->content_is( ... );

Events

The following transaction level events will not be emitted during playback:

pre_freeze =item post_freeze =item resume

SEE ALSO

CONTRIBUTORS

Steve Wagner <truroot at gmail.com>

Joel Berger <joel.a.berger at gmail.com>

Dan Book <grinnz at grinnz.com>

Stefan Adams <stefan@borgia.com>

Everyone on #mojo on irc.perl.org

AUTHOR

Kit Peters <kit.peters@broadbean.com>

COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by Broadbean Technology.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.