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   214444 use Kelp::Base;
  21         53  
  21         122  
4 21     21   11883 use Plack::Test;
  21         16063  
  21         1067  
5 21     21   2656 use Plack::Util;
  21         63543  
  21         581  
6 21     21   11895 use Test::More import => ['!note'];
  21         1841933  
  21         230  
7 21     21   23831 use Test::Deep;
  21         187609  
  21         171  
8 21     21   5432 use Carp;
  21         71  
  21         1201  
9 21     21   3250 use Encode ();
  21         76009  
  21         562  
10 21     21   12481 use HTTP::Cookies;
  21         251296  
  21         760  
11 21     21   168 use Try::Tiny;
  21         49  
  21         1553  
12              
13             BEGIN {
14 21     21   41103 $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 119599 my ( $self, $req ) = @_;
32 201 50       721 croak "HTTP::Request object needed" unless ref($req) eq 'HTTP::Request';
33 201         537 $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       872 $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       88974 if ( $req->uri->opaque =~ qr|^/{1}| ) {
44 200         5783 $req->uri->opaque( '//localhost' . $req->uri->opaque );
45             }
46              
47             # Add the current cookie to the request headers
48 201         10653 $self->cookies->add_cookie_header($req);
49              
50 201     201   43320 my $res = test_psgi( $self->app->run, sub { shift->($req) } );
  201         251354  
51              
52             # Extract the cookies from the response and add them to the cookie jar
53 201         79768 $self->cookies->extract_cookies($res);
54              
55 201         21533 $self->res($res);
56 201         951 return $self;
57             }
58              
59             sub request_ok {
60 9     9 1 10025 my ( $self, $req, $test_name ) = @_;
61 9         26 local $Test::Builder::Level = $Test::Builder::Level + 1;
62              
63 9         27 $self->request($req)->code_is( 200, $test_name );
64             }
65              
66             sub code_is {
67 131     131 1 307 my ( $self, $code, $test_name ) = @_;
68 131         290 local $Test::Builder::Level = $Test::Builder::Level + 1;
69              
70 131   33     681 $test_name ||= "Response code is $code";
71 131         317 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     100881 if ( $code != 500 && $self->res->code == 500 ) {
75 0         0 fail $self->res->content;
76             }
77              
78 131         1802 return $self;
79             }
80              
81             sub code_isnt {
82 2     2 1 8 my ( $self, $code, $test_name ) = @_;
83 2         7 local $Test::Builder::Level = $Test::Builder::Level + 1;
84              
85 2   33     29 $test_name ||= "Response code is not $code";
86 2         9 isnt $self->res->code, $code, $test_name;
87 2         1747 return $self;
88             }
89              
90             sub content_is {
91 100     100 1 259 my ( $self, $value, $test_name ) = @_;
92 100         209 local $Test::Builder::Level = $Test::Builder::Level + 1;
93              
94 100   33     517 $test_name ||= "Content is '$value'";
95 100         264 is Encode::decode( $self->app->charset, $self->res->content ), $value,
96             $test_name;
97 100         75604 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         5 isnt Encode::decode( $self->app->charset, $self->res->content ), $value,
106             $test_name;
107 1         729 return $self;
108             }
109              
110             sub content_like {
111 32     32 1 82 my ( $self, $regexp, $test_name ) = @_;
112 32         64 local $Test::Builder::Level = $Test::Builder::Level + 1;
113              
114 32   66     196 $test_name ||= "Content matches $regexp";
115 32         114 like Encode::decode( $self->app->charset, $self->res->content ), $regexp,
116             $test_name;
117 32         23051 return $self;
118             }
119              
120             sub content_unlike {
121 9     9 1 28 my ( $self, $regexp, $test_name ) = @_;
122 9         23 local $Test::Builder::Level = $Test::Builder::Level + 1;
123              
124 9   66     53 $test_name ||= "Content does not match $regexp";
125 9         34 unlike Encode::decode( $self->app->charset, $self->res->content ), $regexp,
126             $test_name;
127 9         6614 return $self;
128             }
129              
130             sub content_type_is {
131 26     26 1 65 my ( $self, $value, $test_name ) = @_;
132 26         50 local $Test::Builder::Level = $Test::Builder::Level + 1;
133              
134 26   33     150 $test_name ||= "Content-Type is '$value'";
135 26         75 is $self->res->content_type, $value, $test_name;
136 26         18501 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 38 my ( $self, $header, $value, $test_name ) = @_;
150 10         26 local $Test::Builder::Level = $Test::Builder::Level + 1;
151              
152 10   33     70 $test_name ||= "Header '$header' => '$value'";
153 10   33     30 is $self->res->header($header), $value, $test_name
154             || $self->diag_headers();
155 10         8862 return $self;
156             }
157              
158             sub header_isnt {
159 1     1 1 5 my ( $self, $header, $value, $test_name ) = @_;
160 1         4 local $Test::Builder::Level = $Test::Builder::Level + 1;
161              
162 1   33     9 $test_name ||= "Header '$header' is not '$value'";
163 1   33     5 isnt $self->res->header($header), $value, $test_name
164             || $self->diag_headers();
165 1         459 return $self;
166             }
167              
168             sub header_like {
169 1     1 1 4 my ( $self, $header, $regexp, $test_name ) = @_;
170 1         3 local $Test::Builder::Level = $Test::Builder::Level + 1;
171              
172 1   33     12 $test_name ||= "Header '$header' =~ $regexp";
173 1   33     3 like $self->res->header($header), $regexp, $test_name
174             || $self->diag_headers();
175 1         872 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 44 my $self = shift;
190 18 50       59 fail "No JSON decoder" unless $self->app->can('json');
191 18         39 my $result;
192             try {
193 18     18   816 $result = $self->app->json->decode( $self->res->content );
194             }
195             catch {
196 0     0   0 fail("Poorly formatted JSON");
197 18         157 };
198 18         576 return $result;
199             }
200              
201             sub json_cmp {
202 17     17 1 48 my ( $self, $expected, $test_name ) = @_;
203 17         38 local $Test::Builder::Level = $Test::Builder::Level + 1;
204              
205 17   100     66 $test_name ||= "JSON structure matches";
206 17 50       49 like $self->res->header('content-type'), qr/json/, 'Content-Type is JSON'
207             or return $self;
208 17         12056 my $json = $self->json_content;
209 17 50       87 cmp_deeply( $json, $expected, $test_name ) or diag explain $json;
210 17         83198 return $self;
211             }
212              
213             sub note {
214 201     201 1 5731 my $self = shift;
215 201         765 Test::More::note @_;
216 201         117841 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