File Coverage

blib/lib/Test/VCR/LWP.pm
Criterion Covered Total %
statement 160 162 98.7
branch 21 28 75.0
condition 4 6 66.6
subroutine 37 37 100.0
pod 2 9 22.2
total 224 242 92.5


line stmt bran cond sub pod time code
1             package Test::VCR::LWP;
2             $Test::VCR::LWP::VERSION = '0.5';
3 2     2   117991 use strict;
  2         8  
  2         107  
4 2     2   14 use warnings;
  2         4  
  2         84  
5              
6 2     2   22216 use LWP::UserAgent;
  2         94898  
  2         85  
7 2     2   3119 use Data::Dumper;
  2         13467  
  2         168  
8 2     2   1099 use FindBin;
  2         1774  
  2         77  
9 2     2   11 use File::Spec;
  2         2  
  2         26  
10 2     2   7 use Carp;
  2         3  
  2         82  
11              
12 2     2   10 use base 'Exporter';
  2         2  
  2         530  
13             our @EXPORT_OK = qw(withVCR withoutVCR);
14             our $__current_vcr;
15              
16             =head1 NAME
17              
18             Test::VCR::LWP - Record and playback LWP interactions.
19              
20             =head1 VERSION
21              
22             version 0.5
23              
24             =head1 SYNOPSIS
25              
26             withVCR {
27             my $res = $useragent->get('http://metacpan.org/');
28             };
29              
30             =head1 DESCRIPTION
31              
32             Records HTTP transactions done thru LWP to a file and then replays them. Allows
33             your tests suite to be fast, dterministic and accurate.
34              
35             Inspired by (stolen from) L
36              
37             =head1 OO Interface
38            
39             You can use the object oriented interface, but the basic decorator style
40             function interface is recommended.
41              
42             Using the OO interface:
43              
44             my $vcr = Test::VCR::LWP->new(tape => 'mytape.tape');
45            
46             $vcr->run(sub {
47             my $ua = LWP::UserAgent->new;
48             my $res = $ua->get('http://www.perl.org/');
49            
50             if ($_->is_recording) {
51             do_something();
52             }
53             });
54            
55              
56             =cut
57              
58             sub new {
59 32     32 0 42164 my $class = shift;
60 32         324 return bless {@_}, $class;
61             }
62              
63             sub run {
64 23     23 0 9279 my ($self, $code) = @_;
65            
66            
67 23 100       60 if ($self->tape_is_blank) {
68 16         59 $self->record($code);
69             }
70             else {
71 7         27 $self->play($code);
72             }
73             }
74              
75             sub tape_is_blank {
76 23     23 0 46 my ($self) = @_;
77            
78 23         514 return not -s $self->{tape};
79             }
80              
81              
82             sub record {
83 20     20 0 33 my ($self, $code) = @_;
84            
85 20         55 local $self->{is_recording} = 1;
86 20         41 my $original_lwp_request = \&LWP::UserAgent::request;
87            
88 20         61 my $tape = $self->_load_tape;
89            
90 2     2   12 no warnings 'redefine';
  2         2  
  2         534  
91            
92             local *LWP::UserAgent::request = sub {
93 15     15   12187 my ($ua, $req) = @_;
94 15         33 local *LWP::UserAgent::request = $original_lwp_request;
95            
96 15         48 my $res = $original_lwp_request->($ua, $req);
97            
98             # skip recording is often set by the withoutVCR function
99 15 100       2560979 unless ($self->{skip_recording}) {
100 13         77 diag("recording http response for %s %s", $req->method, $req->uri);
101 13         88 push(@$tape, {request => $req, response => $res});
102             }
103             else {
104 2         7 diag("Not recording (within a withoutVCR block).");
105             }
106            
107 15         90 return $res;
108 20         139 };
109            
110 20         29 local $_ = $self;
111 20         22 eval {
112 20         47 $code->();
113             };
114 20         1382 my $e = $@;
115            
116 20 50       2258 open(my $fh, '>', $self->{tape}) || die "Couldn't open $self->{tape}: $!\n";
117            
118 20         57 local $Data::Dumper::Purity = 1;
119 20         162 print $fh "use HTTP::Response;\n";
120 20         59 print $fh "use HTTP::Request;\n";
121 20         131 print $fh Data::Dumper::Dumper($tape), "\n";
122 20 50       7109 close($fh) || die "Couldn't close $self->{tape}: $!\n";
123              
124 20 100       689 die $e if $e;
125             }
126              
127             sub play {
128 7     7 0 18 my ($self, $code) = @_;
129            
130 7         26 $self->_load_tape;
131            
132 2     2   23 no warnings 'redefine';
  2         3  
  2         130  
133 7         26 my @match_fields = ('scheme', 'host', 'port', 'path', 'query');
134 7         15 my $original_lwp_request = \&LWP::UserAgent::request;
135            
136             local *LWP::UserAgent::request = sub {
137 8     8   1634 my ($ua, $incoming) = @_;
138            
139 2     2   8 no warnings 'uninitialized';
  2         2  
  2         1400  
140            
141 8         15 REQ: foreach my $episode (@{$self->{requests}}) {
  8         47  
142 6         44 my $recorded = $episode->{request};
143            
144 6 50       14 next REQ if $recorded->method ne $incoming->method;
145            
146 6         74 foreach my $field (@match_fields) {
147 24 100       777 next REQ if $recorded->uri->$field ne $incoming->uri->$field;
148             }
149            
150 4         104 diag("returning recorded http response for %s %s", $incoming->method, $incoming->uri);
151 4         37 return $episode->{response};
152             }
153            
154 4         53 local *LWP::UserAgent::request = $original_lwp_request;
155            
156 4         6 my $res;
157             $self->record(sub {
158 4         10 $res = $ua->request($incoming);
159 4         30 });
160 4         147 return $res;
161 7         54 };
162            
163 7         12 local $_ = $self;
164 7         19 $code->();
165             }
166              
167              
168             sub is_recording {
169 2     2 0 36 return shift->{is_recording};
170             }
171              
172             sub _load_tape {
173 27     27   36 my ($self) = @_;
174            
175 27 100       262 return [] unless -e $self->{tape};
176            
177 11   66     52 return $self->{requests} ||= do {
178 7         42 local $/;
179 7 50       198 open(my $fh, "<", $self->{tape}) || die "Couldn't open $self->{tape}: $!\n";
180 7         148 my $perl = <$fh>;
181 7 50       54 close($fh) || die "Couldn't close $self->{tape}: $!\n";
182            
183 7         15 our $VAR1;
184 7     1   663 eval "$perl";
  1     1   12  
  1     1   2  
  1     1   36  
  1     1   4  
  1     1   1  
  1     1   196  
  1     1   9  
  1     1   1  
  1     1   36  
  1     1   6  
  1     1   1  
  1     1   365  
  1     1   11  
  1         1  
  1         23  
  1         3  
  1         2  
  1         208  
  1         10  
  1         2  
  1         39  
  1         5  
  1         2  
  1         345  
  1         8  
  1         2  
  1         36  
  1         7  
  1         1  
  1         31  
  1         5  
  1         2  
  1         29  
  1         4  
  1         1  
  1         20  
  1         7  
  1         1  
  1         24  
  1         4  
  1         1  
  1         23  
185            
186 7 50       60 die $@ if $@;
187            
188 7         71 $VAR1;
189             };
190             }
191              
192             sub diag {
193 29     29 0 338 my ($format, @args) = @_;
194            
195 29 50       132 if ($ENV{VCR_DEBUG}) {
196 0         0 my $msg = sprintf($format, @args);
197 0         0 warn "# $msg\n";
198             }
199             }
200              
201             =head2 withVCR
202              
203             Mocks out any calls to LWP::UserAgent with Test::VCR::LWP. Takes a
204             number of flags which are passed to the VCR constructor, and finally
205             a code ref. For example:
206              
207             withVCR {
208             my $req = $ua->post('http://oo.com/object');
209             isa_ok($req, 'HTTP::Response');
210            
211             if ($_->is_recording) {
212             sleep(5);
213             }
214            
215             my $second = $ua->get('http://oo.com/object/' . $res->id);
216            
217             } tape => 'my_test.tape';
218              
219             Or to have the name of the calling sub define the tape name:
220              
221             withVCR {
222             my $req = $ua->post('http://oo.com/object');
223             isa_ok($req, 'HTTP::Response');
224             };
225            
226             The tape file we be placed in the same directory as the script if no tape
227             argument is given. If this function is called outside of a subroutine, the
228             filename of the current perl script will be used to derive a tape filename.
229              
230             Within the code block, C<$_> is the current vcr object. The C
231             method might be useful.
232              
233             =cut
234              
235             sub withVCR (&;@) {
236 8     8 1 2151 my $code = shift;
237 8         24 my %args = @_;
238            
239 8   66     34 $args{tape} ||= do {
240 2         11 my $caller = (caller(1))[3];
241 2         15 $caller =~ s/^.*:://;
242            
243 2 100       8 if ($caller eq '__ANON__') {
244 1         61 croak "tape name must be supplied if called from anonymous subroutine"
245             }
246            
247 1         22 File::Spec->catfile($FindBin::Bin, "$caller.tape");
248             };
249            
250 7         24 my $vcr = Test::VCR::LWP->new(%args);
251 7         47 diag("Created $vcr");
252             # this is so withoutVCR can get to the current vcr object.
253 7         12 local $__current_vcr = $vcr;
254 7         19 $vcr->run($code);
255             }
256              
257             =head2 withoutVCR
258              
259             Allows a section of a withVCR code block to skip recording.
260              
261             withVCR {
262             my $req = $ua->post('http://oo.com/object');
263             isa_ok($req, 'HTTP::Response');
264            
265             withoutVCR {
266             # this will not end up in the tape
267             $ua->post('http://always.com/dothis');
268             };
269             };
270              
271             =cut
272              
273             sub withoutVCR (&) {
274 4     4 1 500 my $code = shift;
275            
276 4 100       16 if (!$__current_vcr) {
277 1         23 croak "Using withoutVCR outside of a withVCR. You probably don't want to do this.";
278             }
279            
280 3         8 local $__current_vcr->{skip_recording} = 1;
281 3         11 diag("Setting skip in $__current_vcr");
282 3         6 $code->();
283             }
284              
285             =head1 TODO
286              
287             =over 2
288              
289             =item *
290              
291             The docs are pretty middling at the moment.
292              
293             =back
294              
295             =head1 AUTHORS
296              
297             Chris Reinhardt
298             crein@cpan.org
299              
300             Mark Ng
301             cpan@markng.co.uk
302            
303             =head1 COPYRIGHT
304              
305             This program is free software; you can redistribute
306             it and/or modify it under the same terms as Perl itself.
307              
308             The full text of the license can be found in the
309             LICENSE file included with this module.
310              
311             =head1 SEE ALSO
312              
313             L, perl(1)
314              
315             =cut
316              
317              
318             1;
319             __END__