File Coverage

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


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