File Coverage

blib/lib/LWP/UserAgent/Mockable.pm
Criterion Covered Total %
statement 96 101 95.0
branch 30 36 83.3
condition 5 5 100.0
subroutine 18 18 100.0
pod 6 6 100.0
total 155 166 93.3


line stmt bran cond sub pod time code
1             package LWP::UserAgent::Mockable;
2              
3 11     11   603308 use warnings;
  11         22  
  11         493  
4 11     11   55 use strict;
  11         20  
  11         289  
5              
6 11     11   6754 use Hook::LexWrap;
  11         18491  
  11         64  
7 11     11   1346 use LWP::UserAgent;
  11         58144  
  11         356  
8 11     11   6751 use Safe::Isa '$_isa';
  11         6154  
  11         1877  
9 11     11   89 use Storable qw( dclone nstore retrieve );
  11         21  
  11         15386  
10              
11             our $VERSION = '1.17';
12              
13             my $instance = __PACKAGE__->__instance;
14             sub __instance {
15 11     11   27 my ( $class ) = @_;
16              
17 11 50       61 if ( not defined $instance ) {
18 11         76 $instance = bless {
19             action => undef,
20             file => undef,
21             current_request => undef,
22             actions => [],
23             callbacks => {},
24             wrappers => {},
25             }, $class;
26              
27             my $action = defined $ENV{ LWP_UA_MOCK }
28             ? lc $ENV{ LWP_UA_MOCK }
29 11 100       78 : 'passthrough';
30              
31 11         65 $instance->reset( $action, $ENV{ LWP_UA_MOCK_FILE } );
32             }
33              
34 9         462 return $instance;
35             }
36              
37             sub reset {
38 22     22 1 124 my ( $class, $action, $file ) = @_;
39              
40 22 50       42 if ( scalar @{ $instance->{ actions } } ) {
  22         272  
41 0         0 die "Can't reset state whilst pending actions. Need to call finish first";
42             }
43              
44 22 100       100 if ( not defined $action ) {
45 9         43 $action = "passthrough";
46             }
47              
48 22 50       175 if ( $action !~ /^(playback|record|passthrough)/ ) {
49 0         0 die "Action must be one of 'passthrough', 'playback' or 'record'";
50             }
51              
52 22 100 100     525 if ( $action ne 'passthrough' and not defined $file ) {
53 1         25 die "No file defined. Should point to file you wish to record to or playback from";
54             }
55              
56 21         56 $instance->{ wrappers } = {};
57 21         85 $instance->{ action } = $action;
58 21         210 $instance->{ file } = $file;
59 21         38 $instance->{ callbacks } = {};
60              
61 21         76 $instance->__reset;
62             }
63              
64             sub __reset {
65 21     21   36 my ( $self ) = @_;
66              
67             my ( $action, $file, $callbacks, $wrappers )
68 21         41 = @{ $self }{ qw( action file callbacks wrappers ) };
  21         83  
69              
70 21 100       89 if ( $action eq 'playback' ) {
71 5         13 local $Storable::Eval = 1;
72              
73 5         29 $self->{ actions } = retrieve( $file );
74              
75             $wrappers->{ pre } = wrap 'LWP::UserAgent::simple_request',
76             pre => sub {
77 22     22   62717 my ( $wrapped, $request ) = @_;
78              
79 22         34 my $current = shift @{ $self->{ actions } };
  22         78  
80 22 50       87 if ( not defined $current ) {
81 0         0 die "No further HTTP requests exist. You possibly need to re-record the LWP session";
82             }
83              
84 22         45 my $response = $current->{ response };
85              
86 22 100       67 if ( $callbacks->{ playback_validation } ) {
87 5         10 my $mock_request = $current->{ request };
88              
89 5         21 $callbacks->{ playback_validation }( $request, $mock_request );
90             }
91              
92 22 100       284 if ( $callbacks->{ playback }) {
93 1         11 $response = $callbacks->{ playback }( $request, $response );
94              
95 1 50       67 if ( not $response->$_isa( 'HTTP::Response' ) ) {
96 0         0 die "playback callback didn't return an HTTP::Response object";
97             }
98             }
99              
100 22         257 $_[ -1 ] = $response;
101 4         30125 };
102             } else {
103             $wrappers->{ pre } = wrap 'LWP::UserAgent::simple_request',
104             pre => sub {
105 31     31   180187 my ( $wrapped, $request ) = @_;
106              
107 31         158 $self->{ current_request } = $request;
108              
109 31 100       300 if ( $callbacks->{ pre_record } ) {
110 9         44 $_[ -1 ] = $callbacks->{ pre_record }( $request );
111              
112 9 100       492 if ( not $_[ -1 ]->$_isa( 'HTTP::Response' ) ) {
113 3         119 die "pre-record callback didn't return an HTTP::Response object";
114             }
115             }
116 16         164 };
117              
118             # It's intentional that wrap is called separately for this. We want the
119             # post action to always be called, even if the pre-action short-circuits
120             # the request. Otherwise, would need to duplicate the storing logic.
121             # This does mean that, when both pre- and post-record callbacks are being
122             # used, that the post-callback will take precedence.
123              
124             $wrappers->{ post } = wrap 'LWP::UserAgent::simple_request',
125             post => sub {
126 28     28   1550157 my $response = $_[ -1 ];
127 28 100       198 if ( $callbacks->{ record }) {
128             $response = $callbacks->{ record }(
129             $self->{ current_request },
130 9         59 $response
131             );
132              
133 9 100       385 if ( not $response->$_isa( 'HTTP::Response' ) ) {
134 3         135 die "record callback didn't return an HTTP::Response object";
135             }
136             }
137              
138 25 100       264 if ( $action eq 'record' ) {
139 15         35 local $Storable::Eval = 1;
140 15         25 local $Storable::Deparse = 1;
141              
142             my $cloned = dclone {
143             request => $self->{ current_request },
144 15         1150 response => $response
145             };
146              
147 15         1814 push @{ $self->{ actions } }, $cloned;
  15         140  
148             }
149 16         1017 };
150             }
151             }
152              
153             sub finished {
154 9     9 1 14410 my ( $class ) = @_;
155              
156 9         39 my $action = $instance->{ action };
157              
158 9 100 100     92 if ( $action eq 'record' ) {
    50          
159 3         37 local $Storable::Deparse = 1;
160 3         8 local $Storable::Eval = 1;
161              
162 3         22 nstore $instance->{ actions }, $instance->{ file };
163 4         28 } elsif ( $action eq 'playback' and scalar @{ $instance->{ actions } } ) {
164 0         0 warn "Not all HTTP requests have been played back. You possibly need to re-record the LWP session";
165             }
166              
167 9         1617 $instance->{ actions } = [];
168 9         177 $instance->{ action } = 'passthrough';
169 9         27 $instance->{ file } = undef;
170              
171 9         60 $instance->reset;
172             }
173              
174             sub set_playback_callback {
175 2     2 1 28713 my ( $class, $cb ) = @_;
176              
177 2         8 $instance->__set_cb( playback => $cb );
178             }
179              
180             sub set_record_callback {
181 15     15 1 8902 my ( $class, $cb ) = @_;
182              
183 15         65 $instance->__set_cb( record => $cb );
184             }
185              
186             sub set_record_pre_callback {
187 12     12 1 7248 my ( $class, $cb ) = @_;
188              
189 12         56 $instance->__set_cb( pre_record => $cb );
190             }
191              
192             sub set_playback_validation_callback {
193 1     1 1 1516 my ( $class, $cb ) = @_;
194              
195 1         9 $instance->__set_cb( playback_validation => $cb );
196             }
197              
198             sub __set_cb {
199 30     30   63 my ( $self, $type, $cb ) = @_;
200              
201 30         138 $self->{ callbacks }{ $type } = $cb;
202             }
203              
204             1;
205              
206             __END__
207              
208             =encoding utf-8
209              
210             =head1 NAME
211              
212             LWP::UserAgent::Mockable - Permits recording, and later playing back of LWP requests.
213              
214             =head1 VERSION
215              
216             Version 1.10
217              
218             =head1 SYNOPSIS
219              
220             In your test code:
221              
222             # setup env vars to control behaviour, allowing them to be
223             # overridden from command line. In current case, do before
224             # loading module, so will be actioned on.
225              
226             BEGIN {
227             $ENV{LWP_UA_MOCK} ||= 'playback';
228             $ENV{LWP_UA_MOCK_FILE} ||= "$0-lwp-mock.out";
229             }
230              
231             use LWP;
232             use LWP::UserAgent::Mockable;
233              
234             # setup a callback when recording, to allow modifying the response
235              
236             LWP::UserAgent::Mockable->set_record_callback( sub {
237             my ( $request, $response ) = @_;
238              
239             print "GOT REQUEST TO: " . $request->uri;
240             $response->content( lc( $response->content ) );
241              
242             return $response;
243             } );
244              
245             # perform LWP request, as normal
246              
247             my $ua = LWP::UserAgent->new;
248             my $res = $ua->get( "http://gmail.com" );
249             print $res->content;
250              
251             # when the LWP work is done, inform LWP::UserAgent::Mockable
252             # that we're finished. Will trigger any behaviour specific to
253             # the action being done, such as saving the recorded session.
254              
255             END {
256             # END block ensures cleanup if script dies early
257             LWP::UserAgent::Mockable->finished;
258             }
259              
260             To run the tests:
261              
262             # Store data
263             LWP_UA_MOCK=record prove t/my-test.t
264              
265             # Use stored data
266             prove t/my-test.t # playback is default in example
267             # or
268             LWP_UA_MOCK=playback prove t/my-test.t
269              
270             # Re-record stored data
271             LWP_UA_MOCK=record prove t/my-test.t
272              
273             # Ignore stored data
274             LWP_UA_MOCK=passthrough prove t/my-test.t
275              
276             =head1 DESCRIPTION
277              
278             This module adds session record and playback options for LWP requests, whilst
279             trying to introduce as little clutter as necessary.
280              
281             When in record mode, all LWP requests and responses will be captured in-memory,
282             until the finished method is called, at which point they will then be written
283             out to a file. In playback mode, LWP responses are short-circuited, to instead
284             return the responses that were previously dumped out. If neither of the above
285             actions are requested, this module does nothing, so LWP requests are handled as
286             normal.
287              
288             Most of the control of this module is done via environment variables, both to
289             control the action being done (LWP_UA_MOCK env var, allowed values being
290             'record', 'playback', 'passthrough' (the default) ), and to control the file
291             that is used for storing or replaying the responses (LWP_UA_MOCK_FILE env var,
292             not used for 'passthrough' mode).
293              
294             The only mandatory change to incorporate this module is to call the 'finished'
295             method, to indicate that LWP processing is completed. Other than that, LWP
296             handling can be done as-normal.
297              
298             As the initial impetus for this module was to allow mocking of external HTTP
299             calls within unit tests, a couple of optional callback (one for each action of
300             the valid action types), to allow for custom handling of responses, or to modify
301             the response that is returned back to the client (this is useful for simulating
302             the requested system being down, or when playing back, to modify the mocked
303             response to reflect expected dynamic content).
304              
305             =head2 Methods
306              
307             As there is only a singleton instance of LWP::UserAgent::Mockable, all methods
308             are class methods.
309              
310             =over 4
311              
312             =item finished() - required
313              
314             Informs LWP::UserAgent::Mockable that no further requests are expected, and
315             allow it to do any post-processing that is required.
316              
317             When in 'record' mode, this will cause the playback file (controlled by
318             LWP_UA_MOCK_FILE env var) to be created. When in 'playback' mode, this will
319             issue a warning if there is still additional mocked responses that haven't been
320             returned.
321              
322             =item set_record_callback( <sub {}> ) - optional
323              
324             =item set_playback_callback( <sub {}> ) - optional
325              
326             These optional methods allow custom callbacks to be inserted, when performing
327             the relevant actions. The callback will be invoked for each LWP request, AFTER
328             the request has been actioned (see set_record_pre_callback for a method o
329             short-circuiting the LWP fetch). They will be passed in 2 parameters, an
330             L<HTTP::Request> and an L<HTTP::Response> object. For the record callback
331             (which is used for both 'record' and 'passthrough' mode) the request will be
332             the L<HTTP::Request> object used to perform the request, and the response the
333             L<HTTP::Response> result from that. In playback mode, the request will be the
334             L<HTTP::Request> object used to perform the request, and the response the mocked
335             response object.
336              
337             When the callbacks are being used, they're expected to return an
338             L<HTTP::Response> object, which will be treated as the actual reply from the
339             call being made. Failure to do do will result in a fatal error being raised.
340              
341             To clear a callback, call the relevant method, passing in no argument.
342              
343             =item set_record_pre_callback( <sub {}> ) - optional
344              
345             This callback is similar to set_record_callback, except that it will
346             short-circuit the actual fetching of the remote URL. Only a single parameter
347             is passed through to this callback, that being the L<HTTP::Request> object.
348             It's expected to construct an return an L<HTTP::Response> object (or subclass
349             thereof). Should anything other than an L<HTTP::Response> subclass be
350             returned, a fatal error will be raised.
351              
352             This callback will be invoked for both 'record' and 'passthrough' modes.
353             Note that there is no analagous callback for 'playback' mode.
354              
355             To clear the callback, pass in no argument.
356              
357             =item set_playback_validation_callback( <sub {}> ) - optional
358              
359             This callback allows validation of the received request. It receives two
360             parameters, both L<HTTP::Request>s, the first being the actual request made,
361             the second being the mocked request that was received when recording a session.
362             It's up to the callback to do any validation that it wants, and to perform any
363             action that is warranted.
364              
365             As with other callbacks, to clear, pass in no argument to the method.
366              
367             =item reset( <action>, <file> ) - optional
368              
369             Reset the state of mocker, allowing the action and file operation on to change.
370             Will also reset all callbacks. Note that this will raise an error, if called
371             whilst there are outstanding requests, and the B<finished> method hasn't been
372             called.
373              
374             =back
375              
376             =head1 CAVEATS
377              
378             The playback file generated by this is not encrypted in any manner. As it's
379             only using L<Storable> to dump the file, it's easy to get at the data contained
380             within, even if the requests are going to HTTPS sites. Treat the playback file
381             as if it were the original data, security-wise.
382              
383             =head1 SEE ALSO
384              
385             =over
386              
387             =item * L<LWP::UserAgent> - The class being mocked.
388              
389             =item * L<Test::LWP::UserAgent>
390              
391             =item * L<HTTP::Request>
392              
393             =item * L<HTTP::Response>
394              
395             =back
396              
397             =head1 AUTHOR
398              
399             Mark Morgan, C<< <makk384@gmail.com> >>
400              
401             =head1 CONTRIBUTORS
402              
403             Michael Jemmeson, C<< <michael.jemmeson at cpan.org> >>
404              
405             Kit Peters, C<< <popefelix at cpan.org> >>
406              
407             Mohammad S. Anwar, C<< <mohammad.anwar at yahoo.com> >>
408              
409             Slaven Rezić, C<< <SREZIC at cpan.org> >>
410              
411             =head1 SUPPORT
412              
413             =head2 Bugs / Feature Requests
414              
415             Please report any bugs or feature requests through the issue tracker
416             at L<https://github.com/mjemmeson/LWP-UserAgent-Mockable/issues>.
417             You will be notified automatically of any progress on your issue.
418              
419             =head2 Source Code
420              
421             This is open source software. The code repository is available for
422             public review and contribution under the terms of the license.
423              
424             L<https://github.com/mjemmeson/LWP-UserAgent-Mockable>
425              
426             git clone git://github.com/mjemmeson/LWP-UserAgent-Mockable.git
427              
428             =head1 COPYRIGHT & LICENSE
429              
430             Copyright 2009 Mark Morgan, all rights reserved.
431              
432             This program is free software; you can redistribute it and/or modify it
433             under the same terms as Perl itself.
434              
435             =cut