File Coverage

blib/lib/Kelp/Test.pm
Criterion Covered Total %
statement 110 128 85.9
branch 8 14 57.1
condition 19 53 35.8
subroutine 27 32 84.3
pod 18 19 94.7
total 182 246 73.9


line stmt bran cond sub pod time code
1              
2             use Kelp::Base;
3 21     21   181248 use Plack::Test;
  21         44  
  21         113  
4 21     21   9858 use Plack::Util;
  21         13839  
  21         972  
5 21     21   2432 use Test::More import => ['!note'];
  21         55952  
  21         537  
6 21     21   9813 use Test::Deep;
  21         1559335  
  21         202  
7 21     21   18379 use Carp;
  21         150013  
  21         163  
8 21     21   4399 use Encode ();
  21         53  
  21         1009  
9 21     21   2710 use HTTP::Cookies;
  21         64228  
  21         562  
10 21     21   9963 use Try::Tiny;
  21         206734  
  21         685  
11 21     21   143  
  21         38  
  21         1406  
12             BEGIN {
13             $ENV{KELP_TESTING} = 1; # Set the ENV for testing
14 21     21   34203 }
15              
16             attr -psgi => undef;
17              
18             attr -app => sub {
19             my $self = shift;
20             return defined $self->psgi
21             ? Plack::Util::load_psgi( $self->psgi )
22             : die "'app' or 'psgi' parameter is required";
23             };
24              
25             attr res => sub { die "res is not initialized" };
26              
27             attr cookies => sub { HTTP::Cookies->new };
28              
29             my ( $self, $req ) = @_;
30             croak "HTTP::Request object needed" unless ref($req) eq 'HTTP::Request';
31 201     201 1 98147 $self->note( $req->method . ' ' . $req->uri );
32 201 50       610  
33 201         452 # Most likely the request was not initialized with a URI that had a scheme,
34             # so we add a default http to prevent unitialized regex matches further
35             # down the chain
36             $req->uri->scheme('http') unless $req->uri->scheme;
37              
38 201 50       761 # If no host was given to the request's uri (most likely), then add
39             # localhost. This is needed by the cookies header, which will not be
40             # applied unless the request uri has a proper domain.
41             if ( $req->uri->opaque =~ qr|^/{1}| ) {
42             $req->uri->opaque( '//localhost' . $req->uri->opaque );
43 201 100       73657 }
44 200         4830  
45             # Add the current cookie to the request headers
46             $self->cookies->add_cookie_header($req);
47              
48 201         8531 my $res = test_psgi( $self->app->run, sub { shift->($req) } );
49              
50 201     201   36379 # Extract the cookies from the response and add them to the cookie jar
  201         205678  
51             $self->cookies->extract_cookies($res);
52              
53 201         62656 $self->res($res);
54             return $self;
55 201         17593 }
56 201         742  
57             my ( $self, $req, $test_name ) = @_;
58             local $Test::Builder::Level = $Test::Builder::Level + 1;
59              
60 9     9 1 8260 $self->request($req)->code_is( 200, $test_name );
61 9         21 }
62              
63 9         21 my ( $self, $code, $test_name ) = @_;
64             local $Test::Builder::Level = $Test::Builder::Level + 1;
65              
66             $test_name ||= "Response code is $code";
67 131     131 1 253 is $self->res->code, $code, $test_name;
68 131         244  
69             # If we got 500 back and shouldn't have, we show the content
70 131   33     568 if ( $code != 500 && $self->res->code == 500 ) {
71 131         258 fail $self->res->content;
72             }
73              
74 131 50 66     87399 return $self;
75 0         0 }
76              
77             my ( $self, $code, $test_name ) = @_;
78 131         1530 local $Test::Builder::Level = $Test::Builder::Level + 1;
79              
80             $test_name ||= "Response code is not $code";
81             isnt $self->res->code, $code, $test_name;
82 2     2 1 6 return $self;
83 2         4 }
84              
85 2   33     23 my ( $self, $value, $test_name ) = @_;
86 2         13 local $Test::Builder::Level = $Test::Builder::Level + 1;
87 2         1575  
88             $test_name ||= "Content is '$value'";
89             is Encode::decode( $self->app->charset, $self->res->content ), $value,
90             $test_name;
91 100     100 1 201 return $self;
92 100         169 }
93              
94 100   33     437 my ( $self, $value, $test_name ) = @_;
95 100         229 local $Test::Builder::Level = $Test::Builder::Level + 1;
96              
97 100         63556 $test_name ||= "Content is not '$value'";
98             isnt Encode::decode( $self->app->charset, $self->res->content ), $value,
99             $test_name;
100             return $self;
101 1     1 1 3 }
102 1         2  
103             my ( $self, $regexp, $test_name ) = @_;
104 1   33     7 local $Test::Builder::Level = $Test::Builder::Level + 1;
105 1         4  
106             $test_name ||= "Content matches $regexp";
107 1         698 like Encode::decode( $self->app->charset, $self->res->content ), $regexp,
108             $test_name;
109             return $self;
110             }
111 32     32 1 77  
112 32         49 my ( $self, $regexp, $test_name ) = @_;
113             local $Test::Builder::Level = $Test::Builder::Level + 1;
114 32   66     142  
115 32         93 $test_name ||= "Content does not match $regexp";
116             unlike Encode::decode( $self->app->charset, $self->res->content ), $regexp,
117 32         18129 $test_name;
118             return $self;
119             }
120              
121 9     9 1 22 my ( $self, $value, $test_name ) = @_;
122 9         15 local $Test::Builder::Level = $Test::Builder::Level + 1;
123              
124 9   66     37 $test_name ||= "Content-Type is '$value'";
125 9         23 is $self->res->content_type, $value, $test_name;
126             return $self;
127 9         4940 }
128              
129             my ( $self, $value, $test_name ) = @_;
130             local $Test::Builder::Level = $Test::Builder::Level + 1;
131 26     26 1 67  
132 26         46 $test_name ||= "Content-Type is not '$value'";
133             isnt $self->res->content_type, $value, $test_name;
134 26   33     131 return $self;
135 26         64 }
136 26         16128  
137             my ( $self, $header, $value, $test_name ) = @_;
138             local $Test::Builder::Level = $Test::Builder::Level + 1;
139              
140 0     0 1 0 $test_name ||= "Header '$header' => '$value'";
141 0         0 is $self->res->header($header), $value, $test_name
142             || $self->diag_headers();
143 0   0     0 return $self;
144 0         0 }
145 0         0  
146             my ( $self, $header, $value, $test_name ) = @_;
147             local $Test::Builder::Level = $Test::Builder::Level + 1;
148              
149 10     10 1 31 $test_name ||= "Header '$header' is not '$value'";
150 10         50 isnt $self->res->header($header), $value, $test_name
151             || $self->diag_headers();
152 10   33     62 return $self;
153 10   33     26 }
154              
155 10         7433 my ( $self, $header, $regexp, $test_name ) = @_;
156             local $Test::Builder::Level = $Test::Builder::Level + 1;
157              
158             $test_name ||= "Header '$header' =~ $regexp";
159 1     1 1 4 like $self->res->header($header), $regexp, $test_name
160 1         2 || $self->diag_headers();
161             return $self;
162 1   33     7 }
163 1   33     4  
164             my ( $self, $header, $regexp, $test_name ) = @_;
165 1         475 local $Test::Builder::Level = $Test::Builder::Level + 1;
166              
167             $test_name ||= "Header '$header' !~ $regexp";
168             unlike $self->res->header($header), $regexp, $test_name
169 1     1 1 3 || $self->diag_headers();
170 1         2 return $self;
171             }
172 1   33     11  
173 1   33     4 my $self = shift;
174             fail "No JSON decoder" unless $self->app->can('json');
175 1         604 my $result;
176             try {
177             $result = $self->app->json->decode( $self->res->content );
178             }
179 0     0 1 0 catch {
180 0         0 fail("Poorly formatted JSON");
181             };
182 0   0     0 return $result;
183 0   0     0 }
184              
185 0         0 my ( $self, $expected, $test_name ) = @_;
186             local $Test::Builder::Level = $Test::Builder::Level + 1;
187              
188             $test_name ||= "JSON structure matches";
189 18     18 0 34 like $self->res->header('content-type'), qr/json/, 'Content-Type is JSON'
190 18 50       49 or return $self;
191 18         31 my $json = $self->json_content;
192             cmp_deeply( $json, $expected, $test_name ) or diag explain $json;
193 18     18   686 return $self;
194             }
195              
196 0     0   0 my $self = shift;
197 18         128 Test::More::note @_;
198 18         498 return $self;
199             }
200              
201             my $self = shift;
202 17     17 1 39 diag $self->res->headers->as_string;
203 17         40 return $self;
204             }
205 17   100     68  
206 17 50       64 my $self = shift;
207             diag $self->res->content;
208 17         11312 return $self;
209 17 50       68 }
210 17         67016  
211             1;
212              
213              
214 201     201 1 4762 =pod
215 201         820  
216 201         100470 =head1 NAME
217              
218             Kelp::Test - Automated tests for a Kelp web app
219              
220 0     0 1   =head1 SYNOPSIS
221 0            
222 0           use MyApp;
223             use Kelp::Test;
224             use HTTP::Request::Common;
225              
226 0     0 1   my $app = MyApp->new;
227 0           my $t = Kelp::Test->new( app => $app );
228 0            
229             $t->request( GET '/path' )
230             ->code_is(200)
231             ->content_is("It works");
232              
233             $t->request( POST '/api' )
234             ->json_cmp({auth => 1});
235              
236             =head1 DESCRIPTION
237              
238             This module provides basic tools for testing a Kelp based web application. It
239             is object oriented, and all methods return C<$self>, so they can be chained
240             together.
241             Testing is done by sending HTTP requests to an already built application and
242             analyzing the response. Therefore, each test usually begins with the L</request>
243             method, which takes a single L<HTTP::Request> parameter. It sends the request to
244             the web app and saves the response as an L<HTTP::Response> object.
245              
246             =head1 ENV VARIABLES
247              
248             =head2 KELP_TESTING
249              
250             This module sets the C<KELP_TESTING> environmental variable to a true value.
251              
252             =head1 ATTRIBUTES
253              
254             =head2 app
255              
256             The Kelp::Test object is instantiated with single attribute called C<app>. It
257             is a reference to a Kelp based web app.
258              
259             my $myapp = MyApp->new;
260             my $t = Kelp::Test->new( app => $myapp );
261              
262             From this point on, all requests run with C<$t-E<gt>request> will be sent to C<$app>.
263              
264             =head2 res
265              
266             Each time C<$t-E<gt>request> is used to send a request, an HTTP::Response object is
267             returned and saved in the C<res> attribute. You can use it to run tests,
268             although as you will see, this module provides methods which make this a lot
269             easier. It is recommended that you use the convenience methods rather than using
270             C<res>.
271              
272             $t->request( GET '/path' )
273             is $t->res->code, 200, "It's a success";
274              
275             =head2 cookies
276              
277             An L<HTTP::Cookies> object containing the cookie jar for all tests.
278              
279             =head1 METHODS
280              
281             =head2 request
282              
283             C<request( $http_request )>
284              
285             Takes an L<HTTP::Request> object and sends it to the application. When the
286             L<HTTP::Response> object is returned, it is initialized in the L</res>
287             attribute.
288             It is very convenient to use L<HTTP::Request::Common> in your test modules, so
289             you can take advantage of the simplified syntax for creating an HTTP request.
290              
291             $t->request( POST '/api', [ user => 'jane' ] );
292              
293             This method returns C<$self>, so other methods can be chained after it.
294              
295             =head2 request_ok
296              
297             C<request_ok( $http_request, $test_name )>
298              
299             Runs C<request>, then tests if the response code is 200. Equivalent to the following
300             code:
301              
302             $t->request( GET '/path' )->code_is(200);
303             $t->request_ok( GET '/path' ); # Same as the above
304              
305             =head2 code_is, code_isnt
306              
307             C<code_is( $code, $test_name )>, C<code_isnt( $code, $test_name )>
308              
309             Tests if the last response returned a status code equal or not equal to C<$code>.
310             An optional name of the test can be added as a second parameter.
311              
312             $t->request( GET '/path' )->code_is(200);
313             $t->request( GET '/path' )->code_isnt(500);
314              
315             =head2 request_ok
316              
317             Same as L</request>, but also runs C<code_is(200)>.
318              
319             $t->request_ok( GET '/home' );
320             # Tests for code = 200
321              
322             =head2 content_is, content_isnt
323              
324             C<content_is( $value, $test_name )>, C<content_isnt( $value, $test_name )>
325              
326             Tests if the last response returned content equal or not equal to C<$value>.
327             An optional name of the test can be added as a second parameter.
328              
329             $t->request( GET '/path' )->content_is("Ok.");
330             $t->request( GET '/path' )->content_isnt("Fail.");
331              
332             =head2 content_like, content_unlike
333              
334             C<content_like( $regexp, $test_name )>, C<content_unlike( $regexp, $test_name )>
335              
336             Tests if the last response returned content that matches or doesn't match C<$regexp>.
337             An optional name of the test can be added as a second parameter.
338              
339             $t->request( GET '/path' )->content_like(qr{Amsterdam});
340             $t->request( GET '/path' )->content_unlike(qr{Rotterdam});
341              
342             =head2 content_type_is, content_type_isnt
343              
344             C<content_type_is( $value, $test_name )>, C<content_type_isnt( $value, $test_name )>
345              
346             Tests if the last response's content-type header is equal or not equal to C<$value>.
347             An optional name of the test can be added as a second parameter.
348              
349             $t->request( GET '/path' )->content_type_is("text/plain");
350             $t->request( GET '/path' )->content_type_isnt("text/html");
351              
352             =head2 header_is, header_isnt
353              
354             C<header_is( $header, $value, $test_name )>, C<header_isnt( $header, $value, $test_name )>
355              
356             Tests if the last response returned a header C<$header> that is equal or not
357             equal to C<$value>. An optional name of the test can be added as a second parameter.
358              
359             $t->request( GET '/path' )->header_is( "Pragma", "no-cache" );
360             $t->request( GET '/path' )->header_isnt( "X-Check", "yes" );
361              
362             =head2 header_like, header_unlike
363              
364             C<header_like( $header, $regexp, $test_name )>, C<header_unlike( $header, $regexp, $test_name )>
365              
366             Tests if the last response returned a header C<$header> that matches or doesn't
367             match C<$regexp>. An optional name of the test can be added as a second parameter.
368              
369             $t->request( GET '/path' )->header_like( "Content-Type", qr/json/ );
370             $t->request( GET '/path' )->header_unlike( "Content-Type", qr/image/ );
371              
372             =head2 json_cmp
373              
374             C<json_cmp( $expected, $test_name )>
375              
376             This tests for two things: If the returned C<content-type> is
377             C<application-json>, and if the returned JSON structure matches the structure
378             specified in C<$expected>. To compare the two structures this method uses
379             C<cmp_deeply> from L<Test::Deep>, so you can use all the goodies from the
380             C<SPECIAL-COMPARISONS-PROVIDED> section of the Test::Deep module.
381              
382             $t->request( GET '/api' )->json_cmp(
383             {
384             auth => 1,
385             timestamp => ignore(),
386             info => subhashof( { name => 'Rick James' } )
387             }
388             );
389              
390             An optional name of the test can be added as a second parameter.
391              
392             =head2 note
393              
394             C<note( $note )>
395              
396             Print a note, using the L<Test::More> C<note> function.
397              
398             $t->request( GET '/path' )
399             ->note("Checking headers now")
400             ->header_is( "Content-Type", qr/json/ );
401              
402             =head2 diag_headers
403              
404             Prints all headers for debugging purposes.
405              
406             $t->request( GET '/path' )
407             ->header_is( "Content-Type", qr/json/ )
408             ->diag_headers();
409              
410             =head2 diag_content
411              
412             Prints the entire content for debugging purposes.
413              
414             $t->request( GET '/path' )
415             ->content_is("Well")
416             ->diag_content();
417              
418             =cut