File Coverage

blib/lib/Kelp/Test.pm
Criterion Covered Total %
statement 133 151 88.0
branch 9 14 64.2
condition 25 65 38.4
subroutine 32 37 86.4
pod 21 21 100.0
total 220 288 76.3


line stmt bran cond sub pod time code
1             package Kelp::Test;
2              
3 29     29   447570 use Kelp::Base;
  29         70  
  29         192  
4 29     29   15028 use Plack::Test;
  29         21546  
  29         1964  
5 29     29   235 use Plack::Util;
  29         63  
  29         873  
6 29     29   16825 use Test::More import => ['!note'];
  29         2678718  
  29         406  
7 29     29   12657 use Test::Deep;
  29         68  
  29         308  
8 29     29   24890 use Kelp::Test::CookieJar;
  29         100  
  29         171  
9 29     29   192 use Carp;
  29         46  
  29         2163  
10 29     29   176 use Try::Tiny;
  29         52  
  29         1709  
11 29     29   155 use Kelp::Util;
  29         67  
  29         1520  
12              
13             BEGIN {
14 29     29   74918 $ENV{KELP_TESTING} = 1; # Set the ENV for testing
15             }
16              
17             sub import
18             {
19 29     29   273 my ($me, @args) = @_;
20              
21 29 100 66     1112 if ($args[0] && $args[0] eq -utf8) {
22 6         39 my $builder = Test::More->builder;
23 6     6   147 binmode $builder->output, ":encoding(utf8)";
  6         6214  
  6         116  
  6         47  
24 6         6235 binmode $builder->failure_output, ":encoding(utf8)";
25 6         1711 binmode $builder->todo_output, ":encoding(utf8)";
26             }
27             }
28              
29             attr -psgi => undef;
30              
31             attr -app => sub {
32             my $self = shift;
33             return defined $self->psgi
34             ? Plack::Util::load_psgi($self->psgi)
35             : die "'app' or 'psgi' parameter is required";
36             };
37             attr charset => undef;
38              
39             attr res => sub { die "res is not initialized" };
40              
41             attr cookies => sub { Kelp::Test::CookieJar->new };
42              
43             sub _decode
44             {
45 220     220   3269 my ($self, $string) = @_;
46 220   66     815 return Kelp::Util::charset_decode($self->charset // $self->app->charset, $string);
47             }
48              
49             sub request
50             {
51 251     251 1 221540 my ($self, $req) = @_;
52 251 50       1082 croak "HTTP::Request object needed" unless ref($req) eq 'HTTP::Request';
53 251         1005 $self->note($req->method . ' ' . $req->uri);
54              
55             # Most likely the request was not initialized with a URI that had a scheme,
56             # so we add a default http to prevent unitialized regex matches further
57             # down the chain
58 251 50       2064 $req->uri->scheme('http') unless $req->uri->scheme;
59              
60             # If no host was given to the request's uri (most likely), then add
61             # localhost. This is needed by the cookies header, which will not be
62             # applied unless the request uri has a proper domain.
63 251 100       147418 if ($req->uri->opaque =~ qr|^/{1}|) {
64 250         9094 $req->uri->opaque('//localhost' . $req->uri->opaque);
65             }
66              
67             # Add the current cookie to the request headers
68 251         16991 $self->cookies->add_cookie_header($req);
69              
70 251     251   1061 my $res = test_psgi($self->app->run, sub { shift->($req) });
  251         363851  
71              
72             # Extract the cookies from the response and add them to the cookie jar
73 251         128038 $self->cookies->extract_cookies($res);
74              
75 251         1024 $self->res($res);
76 251         1310 return $self;
77             }
78              
79             sub request_ok
80             {
81 23     23 1 21948 my ($self, $req, $test_name) = @_;
82 23         57 local $Test::Builder::Level = $Test::Builder::Level + 1;
83              
84 23         71 $self->request($req)->code_is(200, $test_name);
85             }
86              
87             sub code_is
88             {
89 176     176 1 472 my ($self, $code, $test_name) = @_;
90 176         396 local $Test::Builder::Level = $Test::Builder::Level + 1;
91              
92 176   33     1025 $test_name ||= "Response code is $code";
93 176         565 is $self->res->code, $code, $test_name;
94              
95             # If we got 500 back and shouldn't have, we show the content
96 176 50 66     182505 if ($code != 500 && $self->res->code == 500) {
97 0         0 fail $self->res->content;
98             }
99              
100 176         3010 return $self;
101             }
102              
103             sub code_isnt
104             {
105 2     2 1 7 my ($self, $code, $test_name) = @_;
106 2         6 local $Test::Builder::Level = $Test::Builder::Level + 1;
107              
108 2   33     17 $test_name ||= "Response code is not $code";
109 2         6 isnt $self->res->code, $code, $test_name;
110 2         2099 return $self;
111             }
112              
113             sub content_is
114             {
115 116     116 1 412 my ($self, $value, $test_name) = @_;
116 116         267 local $Test::Builder::Level = $Test::Builder::Level + 1;
117              
118 116   33     685 $test_name ||= "Content is '$value'";
119 116         363 is $self->_decode($self->res->content), $value,
120             $test_name;
121 116         133528 return $self;
122             }
123              
124             sub content_bytes_are
125             {
126 11     11 1 32 my ($self, $value, $test_name) = @_;
127 11         22 local $Test::Builder::Level = $Test::Builder::Level + 1;
128              
129 11   33     28 $test_name ||= "Content is '$value'";
130 11         24 my $got = unpack 'H*', $self->res->content;
131 11         163 my $expected = unpack 'H*', $value;
132 11         35 is $got, $expected, $test_name;
133 11         10030 return $self;
134             }
135              
136             sub content_isnt
137             {
138 1     1 1 4 my ($self, $value, $test_name) = @_;
139 1         3 local $Test::Builder::Level = $Test::Builder::Level + 1;
140              
141 1   33     10 $test_name ||= "Content is not '$value'";
142 1         6 isnt $self->_decode($self->res->content), $value,
143             $test_name;
144 1         1500 return $self;
145             }
146              
147             sub content_like
148             {
149 61     61 1 151 my ($self, $regexp, $test_name) = @_;
150 61         123 local $Test::Builder::Level = $Test::Builder::Level + 1;
151              
152 61   66     352 $test_name ||= "Content matches $regexp";
153 61         201 like $self->_decode($self->res->content), $regexp,
154             $test_name;
155 61         43509 return $self;
156             }
157              
158             sub content_unlike
159             {
160 23     23 1 70 my ($self, $regexp, $test_name) = @_;
161 23         63 local $Test::Builder::Level = $Test::Builder::Level + 1;
162              
163 23   66     173 $test_name ||= "Content does not match $regexp";
164 23         94 unlike $self->_decode($self->res->content), $regexp,
165             $test_name;
166 23         15580 return $self;
167             }
168              
169             sub content_type_is
170             {
171 40     40 1 115 my ($self, $value, $test_name) = @_;
172 40         89 local $Test::Builder::Level = $Test::Builder::Level + 1;
173              
174 40   33     229 $test_name ||= "Content-Type is '$value'";
175 40         140 is $self->res->content_type, $value, $test_name;
176 40         32053 return $self;
177             }
178              
179             sub full_content_type_is
180             {
181 6     6 1 27 my ($self, $value, $test_name) = @_;
182 6         14 local $Test::Builder::Level = $Test::Builder::Level + 1;
183              
184 6   33     40 $test_name ||= "Full Content-Type is '$value'";
185 6         19 is join('; ', $self->res->content_type), $value, $test_name;
186 6         8623 return $self;
187             }
188              
189             sub content_type_isnt
190             {
191 0     0 1 0 my ($self, $value, $test_name) = @_;
192 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
193              
194 0   0     0 $test_name ||= "Content-Type is not '$value'";
195 0         0 isnt $self->res->content_type, $value, $test_name;
196 0         0 return $self;
197             }
198              
199             sub header_is
200             {
201 24     24 1 106 my ($self, $header, $value, $test_name) = @_;
202 24         55 local $Test::Builder::Level = $Test::Builder::Level + 1;
203              
204 24   33     171 $test_name ||= "Header '$header' => '$value'";
205 24   33     86 is $self->res->header($header), $value, $test_name
206             || $self->diag_headers();
207 24         28675 return $self;
208             }
209              
210             sub header_isnt
211             {
212 3     3 1 14 my ($self, $header, $value, $test_name) = @_;
213 3         9 local $Test::Builder::Level = $Test::Builder::Level + 1;
214              
215 3   33     28 $test_name ||= "Header '$header' is not '$value'";
216 3   33     13 isnt $self->res->header($header), $value, $test_name
217             || $self->diag_headers();
218 3         2746 return $self;
219             }
220              
221             sub header_like
222             {
223 1     1 1 4 my ($self, $header, $regexp, $test_name) = @_;
224 1         4 local $Test::Builder::Level = $Test::Builder::Level + 1;
225              
226 1   33     11 $test_name ||= "Header '$header' =~ $regexp";
227 1   33     5 like $self->res->header($header), $regexp, $test_name
228             || $self->diag_headers();
229 1         939 return $self;
230             }
231              
232             sub header_unlike
233             {
234 0     0 1 0 my ($self, $header, $regexp, $test_name) = @_;
235 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
236              
237 0   0     0 $test_name ||= "Header '$header' !~ $regexp";
238 0   0     0 unlike $self->res->header($header), $regexp, $test_name
239             || $self->diag_headers();
240 0         0 return $self;
241             }
242              
243             sub json_content
244             {
245 19     19 1 54 my $self = shift;
246 19         67 my $result;
247 19         102 my $decoder = $self->app->get_encoder(json => 'internal');
248             try {
249 19     19   1264 $result = $decoder->decode(
250             $self->_decode($self->res->content)
251             );
252             }
253             catch {
254 0     0   0 fail("Poorly formatted JSON");
255 19         228 };
256 19         2466 return $result;
257             }
258              
259             sub json_cmp
260             {
261 18     18 1 159 my ($self, $expected, $test_name) = @_;
262 18         73 local $Test::Builder::Level = $Test::Builder::Level + 1;
263              
264 18   100     123 $test_name ||= "JSON structure matches";
265 18 50       109 like $self->res->header('content-type'), qr/json/, 'Content-Type is JSON'
266             or return $self;
267 18         21634 my $json = $self->json_content;
268 18 50       138 cmp_deeply($json, $expected, $test_name) or diag explain $json;
269 18         121005 return $self;
270             }
271              
272             sub note
273             {
274 251     251 1 8533 my $self = shift;
275 251         1235 Test::More::note @_;
276 251         187990 return $self;
277             }
278              
279             sub diag_headers
280             {
281 0     0 1 0 my $self = shift;
282 0         0 diag $self->res->headers->as_string;
283 0         0 return $self;
284             }
285              
286             sub diag_content
287             {
288 0     0 1 0 my $self = shift;
289 0         0 diag $self->res->content;
290 0         0 return $self;
291             }
292              
293             1;
294              
295             __END__
296              
297             =pod
298              
299             =head1 NAME
300              
301             Kelp::Test - Automated tests for a Kelp web app
302              
303             =head1 SYNOPSIS
304              
305             use MyApp;
306             use Kelp::Test;
307             use HTTP::Request::Common;
308              
309             my $app = MyApp->new;
310             my $t = Kelp::Test->new( app => $app );
311              
312             $t->request( GET '/path' )
313             ->code_is(200)
314             ->content_is("It works");
315              
316             $t->request( POST '/api' )
317             ->json_cmp({auth => 1});
318              
319             # automatically sets wide output for Test::More (disables Wide character warnings)
320             use Kelp::Test -utf8;
321              
322             =head1 DESCRIPTION
323              
324             This module provides basic tools for testing a Kelp based web application. It
325             is object oriented, and all methods return C<$self>, so they can be chained
326             together.
327             Testing is done by sending HTTP requests to an already built application and
328             analyzing the response. Therefore, each test usually begins with the L</request>
329             method, which takes a single L<HTTP::Request> parameter. It sends the request to
330             the web app and saves the response as an L<HTTP::Response> object.
331              
332             =head1 ENV VARIABLES
333              
334             =head2 KELP_TESTING
335              
336             This module sets the C<KELP_TESTING> environmental variable to a true value.
337              
338             =head1 ATTRIBUTES
339              
340             =head2 app
341              
342             The Kelp::Test object is instantiated with single attribute called C<app>. It
343             is a reference to a Kelp based web app.
344              
345             my $myapp = MyApp->new;
346             my $t = Kelp::Test->new( app => $myapp );
347              
348             From this point on, all requests run with C<$t-E<gt>request> will be sent to C<$app>.
349              
350             =head2 charset
351              
352             The charset to use for decoding the response. By default, application's charset
353             will be used. Use it if some responses are in a different charset. Can be
354             cleared by setting it back to undef.
355              
356             =head2 res
357              
358             Each time C<$t-E<gt>request> is used to send a request, an HTTP::Response object is
359             returned and saved in the C<res> attribute. You can use it to run tests,
360             although as you will see, this module provides methods which make this a lot
361             easier. It is recommended that you use the convenience methods rather than using
362             C<res>.
363              
364             $t->request( GET '/path' )
365             is $t->res->code, 200, "It's a success";
366              
367             =head2 cookies
368              
369             An object of C<Kelp::Test::CookieJar> implementing the partial interface of
370             L<HTTP::Cookies> module, containing the cookie jar for all tests. Compared to
371             the module it's mocking, it does not handle cookie parameters other than name
372             and value, but properly escapes the cookie name and value for the request.
373             Its usage should usually be as trivial as this:
374              
375             # NOTE: extra undef parameters are required to match HTTP::Cookies interface
376              
377             $t->set_cookie(undef, $name, $value);
378             $t->request(...);
379              
380             my $cookies_hash = $t->get_cookies;
381             my @cookie_values = $t->get_cookies(undef, 'cookie1', 'cookie2');
382              
383             =head1 METHODS
384              
385             =head2 request
386              
387             C<request( $http_request )>
388              
389             Takes an L<HTTP::Request> object and sends it to the application. When the
390             L<HTTP::Response> object is returned, it is initialized in the L</res>
391             attribute.
392             It is very convenient to use L<HTTP::Request::Common> in your test modules, so
393             you can take advantage of the simplified syntax for creating an HTTP request.
394              
395             $t->request( POST '/api', [ user => 'jane' ] );
396              
397             This method returns C<$self>, so other methods can be chained after it.
398              
399             =head2 request_ok
400              
401             C<request_ok( $http_request, $test_name )>
402              
403             Runs C<request>, then tests if the response code is 200. Equivalent to the following
404             code:
405              
406             $t->request( GET '/path' )->code_is(200);
407             $t->request_ok( GET '/path' ); # Same as the above
408              
409             =head2 code_is, code_isnt
410              
411             C<code_is( $code, $test_name )>, C<code_isnt( $code, $test_name )>
412              
413             Tests if the last response returned a status code equal or not equal to C<$code>.
414             An optional name of the test can be added as a second parameter.
415              
416             $t->request( GET '/path' )->code_is(200);
417             $t->request( GET '/path' )->code_isnt(500);
418              
419             =head2 request_ok
420              
421             Same as L</request>, but also runs C<code_is(200)>.
422              
423             $t->request_ok( GET '/home' );
424             # Tests for code = 200
425              
426             =head2 content_is, content_isnt
427              
428             C<content_is( $value, $test_name )>, C<content_isnt( $value, $test_name )>
429              
430             Tests if the last response returned content equal or not equal to C<$value>.
431             An optional name of the test can be added as a second parameter.
432              
433             $t->request( GET '/path' )->content_is("Ok.");
434             $t->request( GET '/path' )->content_isnt("Fail.");
435              
436             =head2 content_bytes_are
437              
438             Same as C<content_is>, but the result is not decoded and the values are
439             compared byte by byte as hex-encoded string.
440              
441             =head2 content_like, content_unlike
442              
443             C<content_like( $regexp, $test_name )>, C<content_unlike( $regexp, $test_name )>
444              
445             Tests if the last response returned content that matches or doesn't match C<$regexp>.
446             An optional name of the test can be added as a second parameter.
447              
448             $t->request( GET '/path' )->content_like(qr{Amsterdam});
449             $t->request( GET '/path' )->content_unlike(qr{Rotterdam});
450              
451             =head2 content_type_is, content_type_isnt
452              
453             C<content_type_is( $value, $test_name )>, C<content_type_isnt( $value, $test_name )>
454              
455             Tests if the last response's content-type header is equal or not equal to C<$value>.
456             An optional name of the test can be added as a second parameter.
457              
458             $t->request( GET '/path' )->content_type_is("text/plain");
459             $t->request( GET '/path' )->content_type_isnt("text/html");
460              
461             =head2 full_content_type_is
462              
463             Like L</content_type_is>, but checks the full content type (with charset).
464              
465             =head2 header_is, header_isnt
466              
467             C<header_is( $header, $value, $test_name )>, C<header_isnt( $header, $value, $test_name )>
468              
469             Tests if the last response returned a header C<$header> that is equal or not
470             equal to C<$value>. An optional name of the test can be added as a second parameter.
471              
472             $t->request( GET '/path' )->header_is( "Pragma", "no-cache" );
473             $t->request( GET '/path' )->header_isnt( "X-Check", "yes" );
474              
475             =head2 header_like, header_unlike
476              
477             C<header_like( $header, $regexp, $test_name )>, C<header_unlike( $header, $regexp, $test_name )>
478              
479             Tests if the last response returned a header C<$header> that matches or doesn't
480             match C<$regexp>. An optional name of the test can be added as a second parameter.
481              
482             $t->request( GET '/path' )->header_like( "Content-Type", qr/json/ );
483             $t->request( GET '/path' )->header_unlike( "Content-Type", qr/image/ );
484              
485             =head2 json_content
486              
487             C<json_content()>
488              
489             Returns the content decoded as JSON. Does not perform any checks, but may
490             C<fail()> and return C<undef> if the JSON decoding fails.
491              
492             =head2 json_cmp
493              
494             C<json_cmp( $expected, $test_name )>
495              
496             This tests for two things: If the returned C<content-type> is
497             C<application-json>, and if the returned JSON structure matches the structure
498             specified in C<$expected>. To compare the two structures this method uses
499             C<cmp_deeply> from L<Test::Deep>, so you can use all the goodies from the
500             C<SPECIAL-COMPARISONS-PROVIDED> section of the Test::Deep module.
501              
502             $t->request( GET '/api' )->json_cmp(
503             {
504             auth => 1,
505             timestamp => ignore(),
506             info => subhashof( { name => 'Rick James' } )
507             }
508             );
509              
510             An optional name of the test can be added as a second parameter.
511              
512             =head2 note
513              
514             C<note( $note )>
515              
516             Print a note, using the L<Test::More> C<note> function.
517              
518             $t->request( GET '/path' )
519             ->note("Checking headers now")
520             ->header_is( "Content-Type", qr/json/ );
521              
522             =head2 diag_headers
523              
524             Prints all headers for debugging purposes.
525              
526             $t->request( GET '/path' )
527             ->header_is( "Content-Type", qr/json/ )
528             ->diag_headers();
529              
530             =head2 diag_content
531              
532             Prints the entire content for debugging purposes.
533              
534             $t->request( GET '/path' )
535             ->content_is("Well")
536             ->diag_content();
537              
538             =cut
539