File Coverage

blib/lib/Test/WWW/Declare.pm
Criterion Covered Total %
statement 147 163 90.1
branch 62 76 81.5
condition 7 8 87.5
subroutine 36 40 90.0
pod 22 32 68.7
total 274 319 85.8


line stmt bran cond sub pod time code
1             package Test::WWW::Declare;
2 14     14   77 use warnings;
  14         29  
  14         455  
3 14     14   81 use strict;
  14         27  
  14         471  
4 14     14   76 use base 'Test::More';
  14         29  
  14         17011  
5 14     14   130738 use Test::WWW::Mechanize;
  14         3218586  
  14         690  
6 14     14   175 use Test::Builder;
  14         28  
  14         34226  
7              
8             our $VERSION = '0.02';
9              
10             our @EXPORT = qw(flow run get session check mech match follow_link content
11             should shouldnt click href button fill form SKIP _twd_dummy
12             title equal caselessly contain matches equals contains
13             never always lack lacks url uri);
14             our $BUILDER = Test::Builder->new();
15             our $WWW_MECHANIZE;
16             our $IN_FLOW;
17             our %mechs;
18              
19             =begin private
20              
21             =head2 import_extra
22              
23             Called by L's C code when L is first
24             C'd, it asks Test::More to export its symbols to the namespace that
25             C'd this one.
26              
27             =end private
28              
29             =cut
30              
31             sub import_extra {
32 14     14 1 4885 Test::More->export_to_level(2);
33             }
34              
35             =head1 NAME
36              
37             Test::WWW::Declare - declarative testing for your web app
38              
39             =head1 SYNOPSIS
40              
41             use Test::WWW::Declare tests => 3;
42             use Your::Web::App::Test;
43              
44             Your::Web::App::Test->start_server;
45              
46             session 'testuser' => run {
47             flow 'log in and out' => check {
48             flow 'log in' => check {
49             get 'http://localhost/';
50             fill form 'login' => {
51             username => 'testuser',
52             password => 'drowssap',
53             };
54             content should contain 'log out';
55             };
56              
57             flow 'log out' => check {
58             get 'http://localhost/';
59             click href 'log out';
60             };
61             };
62             };
63              
64             =head1 DESCRIPTION
65              
66             Often in web apps, tests are very dependent on the state set up by previous
67             tests. If one test fails (e.g. "follow the link to the admin page") then it's
68             likely there will be many more failures. This module aims to alleviate this
69             problem, as well as provide a nicer interface to L.
70              
71             The central idea is that of "flow". Each flow is a sequence of commands ("fill
72             in this form") and assertions ("content should contain 'testuser'"). If any of
73             these commands or assertions fail then the flow is aborted. Only that one
74             failure is reported to the test harness and user. Flows may also contain other
75             flows. If an inner flow fails, then the outer flow fails as well.
76              
77             =head1 FLOWS AND SESSIONS
78              
79             =head2 session NAME => run { CODE }
80              
81             Sessions are a way of associating a set of flows with a L
82             instance. A session is mostly equivalent with a user interacting with your web
83             app.
84              
85             Within a session, every command (C, C, etc) is operating on
86             that session's L instance. You may have multiple sessions in
87             one test file. Two sessions with the same name are in fact the same session.
88             This lets you write code like the following, simplified slightly:
89              
90             session 'first user' => run {
91             get "$URL/give?task=1&victim=other";
92             session 'other user' => run {
93             get "$URL/tasks";
94             content should match qr/task 1/;
95              
96             # this is the same session/mech as the outermost 'first user'
97             session 'first user' => run {
98             get "$URL/tasks";
99             content shouldnt match qr/task 1/;
100             };
101             };
102             };
103              
104             =head2 flow NAME => check { CODE }
105              
106             A flow encompasses a single test. As described above, each flow is a sequence
107             of commands, assertions, and other flows. If any of the components of a flow
108             fail, the rest of the flow is aborted and one or more test failures are
109             reported to the test harness.
110              
111             =head1 COMMANDS
112              
113             =head2 get URL
114              
115             =head2 click button
116              
117             =head2 click href
118              
119             =head2 follow_link
120              
121             =head2 fill form NAME => {FIELD1 => VALUE1, FIELD2 => VALUE2}
122              
123             =head1 ASSERTIONS
124              
125             Every assertion has two parts: a subject and a verb.
126              
127             =head2 SUBJECTS
128              
129             =head3 content
130              
131             =head3 title
132              
133             =head3 url
134              
135             =head2 VERBS
136              
137             =head3 should(nt) (caselessly) match REGEX
138              
139             =head3 should(nt) (caselessly) contain STRING
140              
141             =head3 should(nt) (caselessly) lack STRING
142              
143             =head3 should(nt) (caselessly) equal STRING
144              
145             =cut
146              
147             # DSLey functions
148 0     0 1 0 sub to($) { return $_[0] }
149              
150             sub _args {
151 74     74   117 my $args = shift;
152 74 100       330 return $args if ref($args) eq 'HASH';
153 37         139 return {expected => $args};
154             }
155              
156             sub should ($) {
157 29     29 1 81 return _args(shift);
158             }
159              
160             sub shouldnt ($) {
161 4     4 0 9 my $args = _args(shift);
162 4         8 $args->{negative} = 1;
163 4         15 return $args;
164             }
165              
166             sub match ($) {
167 11     11 1 246 my $args = _args(shift);
168 11         43 $args->{match} = 'regex';
169 11         48 return $args;
170             }
171              
172             sub equal ($) {
173 18     18 1 268 my $args = _args(shift);
174 18         48 $args->{match} = 'equality';
175 18         73 return $args;
176             }
177              
178             sub contain ($) {
179 7     7 1 15 my $args = _args(shift);
180 7         214 $args->{match} = 'index';
181 7         26 return $args;
182             }
183              
184             sub lack ($) {
185 1     1 1 5 my $args = _args(shift);
186 1         2 $args->{match} = 'index';
187 1         2 $args->{negative} = 1;
188 1         4 return $args;
189             }
190              
191             sub caselessly ($) {
192 4     4 0 10 my $args = _args(shift);
193 4         8 $args->{case_insensitive} = 1;
194 4         861 return $args;
195             }
196              
197             sub check (&) {
198 29     29 1 188 my $coderef = shift;
199              
200 29         153 return $coderef;
201             }
202              
203             sub run (&) {
204 17     17 1 3637 my $coderef = shift;
205              
206 17         213 return $coderef;
207             }
208              
209             # alternates (e.g. "foo matches bar" instead of "foo should match bar")
210 3     3 0 8 sub contains ($) { contain $_[0] }
211 2     2 0 9 sub equals ($) { equal $_[0] }
212 1     1 0 46 sub matches ($) { match $_[0] }
213 0     0 0 0 sub lacks ($) { lack $_[0] }
214              
215 1     1 0 4 sub always ($) { should $_[0] }
216 1     1 0 5 sub never ($) { shouldnt $_[0] }
217              
218             # Mech interactions
219             sub mech(;$) {
220 93     93 0 196 my $name = shift;
221 93 100       977 return defined $name ? $mechs{$name} : $WWW_MECHANIZE;
222             }
223              
224             sub get {
225 18     18 1 257 my $url = shift;
226              
227 18         78 mech()->get($url);
228 18 100       1833171 if (!$IN_FLOW)
229             {
230 1         5 $BUILDER->ok(mech->success, "navigated to $url");
231             }
232              
233 18 50       350 return if mech->success;
234              
235 0 0       0 Carp::croak mech->status
236             . (mech->response ? ' - ' . mech->response->message : '')
237             }
238              
239             sub href ($) {
240 11     11 1 251 return (shift, 'href');
241             }
242              
243             sub button ($) {
244 2     2 1 30 return (shift, 'button');
245             }
246              
247             sub click {
248 13     13 1 32 my $link = shift;
249 13         24 my $type = shift;
250              
251 13 100       54 if ($type eq 'button') {
252 2         5 my $ok = mech()->click_button(value => $link);
253 2 50       23512 $ok = $ok->is_success if $ok;
254 2 50       24 my $verb = ref($link) eq 'Regexp' ? "matching " : "";
255 2 50       8 $BUILDER->ok($ok, "Clicked button $verb$link") if !$IN_FLOW;
256 2         10 return $ok;
257             }
258             else {
259 11 100       59 if (ref $link ne 'Regexp') {
260 1         338 Carp::croak "click doesn't know what to do with a link type of "
261             . ref($link);
262             }
263 10         16 my $ok;
264 10         33 my $response = mech()->follow_link(text_regex => $link);
265 10 100 100     144308 $ok = 1 if $response && $response->is_success;
266 10 100       162 $BUILDER->ok($ok, "Clicked link matching $link") if !$IN_FLOW;
267 10 100       623 Carp::croak($response ? $response->as_string : "No link matching $link found") if !$ok;
    100          
268 8         43 return $ok;
269             }
270             }
271              
272             sub follow_link {
273 0     0 1 0 my $ret = mech()->follow_link(@_);
274              
275 0 0       0 if (!$ret) {
276 0         0 Carp::croak "follow_link couldn't find a link matching "
277             . "(" . join(', ', @_) . ")";
278             }
279             }
280              
281             sub content ($) {
282 10     10 1 32 _magic_match({got => mech()->content, name => "Content", %{shift @_}});
  10         455  
283             }
284              
285             sub title ($) {
286 26     26 1 92 my $title = mech()->title;
287 26         6027 _magic_match({got => $title, name => "Title '$title'", %{shift @_}});
  26         210  
288             }
289              
290             sub url ($) {
291 1     1 1 4 my $url = mech()->uri;
292 1         28 _magic_match({got => $url, name => "URL '$url'", %{shift @_}});
  1         11  
293             }
294             *uri = \&url;
295              
296             # yes, there's a little too much logic in here. that's why it's magic
297             sub _magic_match {
298 37     37   74 my $orig = shift @_;
299 37         200 my %args = %$orig;
300 37         81 my $match;
301             my @output;
302              
303 37   100     319 $args{negative} ||= 0;
304              
305 37         72 push @output, $args{name};
306 37 100       118 push @output, $args{negative} ? ()
307             : "does not";
308              
309 37 100       161 if ($args{match} eq 'equality') {
    100          
    50          
310 18 100       56 if ($args{case_insensitive}) {
311 2         6 push @output, "caselessly";
312 2         6 $args{got} = lc $args{got};
313 2         6 $args{expected} = lc $args{expected};
314             }
315              
316 18 100       54 push @output, $args{negative} ? "equals"
317             : "equal";
318 18         40 push @output, $orig->{expected};
319              
320 18         53 $match = $args{got} eq $args{expected};
321             }
322             elsif ($args{match} eq 'index') {
323 8 100       26 if ($args{case_insensitive}) {
324 2         6 push @output, "caselessly";
325 2         5 $args{got} = lc $args{got};
326 2         7 $args{expected} = lc $args{expected};
327             }
328              
329 8 100       22 push @output, $args{negative} ? "contains"
330             : "contain";
331 8         48 push @output, $orig->{expected};
332              
333 8         22 $match = index($args{got}, $args{expected}) >= 0;
334             }
335             elsif ($args{match} eq 'regex') {
336 11 50       47 if ($args{case_insensitive}) {
337 0         0 push @output, "caselessly";
338 0         0 push @output, $args{expected};
339 0         0 $args{expected} = "(?i:$args{expected})";
340             }
341              
342 11 100       49 push @output, $args{negative} ? "matches"
343             : "match";
344 11         30 push @output, $orig->{expected};
345              
346 11         127 $match = $args{got} =~ $args{expected};
347             }
348             else {
349 0         0 Carp::croak "No \$args{match} (yes this error needs to be fixed)";
350             }
351              
352 37 100       140 my $ok = ($match ? 1 : 0) ^ $args{negative};
353 37 100       104 if (!$IN_FLOW) {
354 2         18 $BUILDER->ok($ok, join(' ', @output));
355 2         323 return $ok;
356             }
357              
358 35 50       189 return 1 if $ok;
359 0         0 Carp::croak join(' ', @output);
360             }
361              
362             sub form ($$) {
363 3     3 1 81 my $form_name = shift;
364 3         7 my $data = shift;
365              
366 3         12 my $form = mech()->form_name($form_name);
367              
368 3 100       21205 if (!defined($form)) {
369 1         240 Carp::croak "There is no form named '$form_name'";
370             }
371              
372 2         13 return $data;
373             }
374              
375             sub fill {
376 2     2 1 6 my $data = shift;
377              
378 2 50       11 Carp::croak "fill expects a hashref" if ref($data) ne 'HASH';
379              
380 2         7 mech()->set_fields(%{$data});
  2         20  
381             }
382              
383             # the meat of the module
384             sub SKIP ($) {
385 2     2 1 45 my $reason = shift;
386              
387 2         555 Carp::croak "SKIP: $reason";
388             }
389              
390             sub flow ($$) {
391 29     29 1 61 my $name = shift;
392 29         56 my $coderef = shift;
393              
394 29         66 eval { local $IN_FLOW = 1; $coderef->() };
  29         51  
  29         82  
395              
396 29 100       1823 if ($@ =~ /^SKIP: (.*)$/) {
    100          
397 2         7 my $reason = $1;
398 2         28 $BUILDER->skip($reason);
399             }
400             elsif ($@) {
401 5 100       19 if ($IN_FLOW) {
402 1 50       13 if ($@ =~ /^Flow '/)
403             {
404 0         0 die $@;
405             }
406 1         7 die "Flow '$name' failed: $@";
407             }
408              
409 4         49 $BUILDER->ok(0, $name);
410              
411 4 100       773 if ($@ =~ /^Flow '/) {
412 1         11 $BUILDER->diag($@);
413             }
414             else {
415 3         34 $BUILDER->diag("Flow '$name' failed: $@");
416             }
417             }
418             else {
419 22         263 $BUILDER->ok(1, $name);
420             }
421             }
422              
423             sub session ($$) {
424 17     17 1 44 my $title = shift;
425 17         37 my $coderef = shift;
426              
427 17   66     456 $mechs{$title} ||= Test::WWW::Mechanize->new(quiet => 1);
428 17         341628 local $WWW_MECHANIZE = $mechs{$title};
429              
430 17         2076 $coderef->();
431              
432 17 50       2938 if ($@ =~ /^SKIP: (.*)$/) {
    100          
    100          
433 0         0 my $reason = $1;
434 0         0 $BUILDER->skip($reason);
435             }
436             elsif ($@ =~ /^Flow '/) {
437             # flow already displayed the error
438             }
439             elsif ($@) {
440 1         6 $BUILDER->diag($@);
441             }
442             }
443              
444             sub dump($) {
445 0     0 0 0 my $file = shift;
446 0         0 mech->save_content($file);
447             }
448              
449             # used only for testing that we got T:W:D's goods
450 1     1   2027 sub _twd_dummy { "XYZZY" }
451              
452             =head1 SUBCLASSING
453              
454             One of the goals of this module is to let you subclass it to provide extra
455             features, such as automatically logging in a user each time a session is
456             created.
457              
458             =head1 CAVEATS
459              
460             If you fail any tests, then the actual number of tests run may be fewer than
461             you have in your file. This is because when a flow fails, it immediately aborts
462             the rest of its body (which may include other flows). So if you're setting the
463             number of tests based on how many ran, make sure that all tests passed.
464              
465             =head1 BUGS
466              
467             Hopefully few. We'd like to know about any of them. Please report them to
468             C.
469              
470             =head1 SEE ALSO
471              
472             L, L.
473              
474             =head1 MAINTAINER
475              
476             Shawn M Moore C<< >>
477              
478             =head1 ORIGINAL AUTHOR
479              
480             Jesse Vincent C<< >>
481              
482             =head1 COPYRIGHT
483              
484             Copyright 2007-2008 Best Practical Solutions, LLC
485              
486             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
487              
488             =cut
489              
490             1;
491