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