File Coverage

blib/lib/Test/Story.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Test::Story;
2 1     1   54962 use Moose;
  0            
  0            
3             use Test::FITesque::Suite;
4             use Test::FITesque::Test;
5             use Test::Story::File;
6             use File::Find;
7             use Storable qw(dclone);
8              
9             our $VERSION = '0.07';
10              
11             sub BUILD {
12             my $self = shift;
13             my %defaults = (
14             fixture_base => "Fixture",
15             file_root => "cases",
16             filenames => [],
17             verbose => 0,
18             allowed_extensions => [qw( tc st )],
19             tags => {
20             include => [],
21             exclude => [],
22             },
23             );
24             foreach my $key (keys %defaults) {
25             next if exists $self->config->{$key};
26             $self->config->{$key} = $defaults{$key};
27             }
28             }
29              
30             has config => (
31             is => q{ro},
32             required => 1,
33             isa => q{HashRef}
34             );
35              
36             my %default_lazy = (
37             required => 1,
38             lazy => 1,
39             is => q{ro},
40             default => sub { die "need to override" },
41             );
42              
43             has verbose => (
44             %default_lazy,
45             isa => q{Int},
46             default => sub { return shift->config->{verbose} },
47             );
48              
49             has testcase_id => (
50             %default_lazy,
51             isa => q{Str},
52             default => sub { return shift->config->{testcase_id} || '' },
53             );
54              
55             has filenames => (
56             %default_lazy,
57             isa => q{ArrayRef},
58             default => sub { return shift->config->{filenames} },
59             );
60              
61             has file_root => (
62             %default_lazy,
63             isa => q{Str},
64             default => sub { return shift->config->{file_root} },
65             );
66              
67             has fixture_base => (
68             %default_lazy,
69             isa => q{Str},
70             default => sub { return shift->config->{fixture_base} },
71             );
72              
73             has allowed_extensions => (
74             %default_lazy,
75             isa => q{ArrayRef},
76             default => sub { return shift->config->{allowed_extensions} },
77             );
78              
79             has file_paths => (
80             is => q{ro},
81             required => 1,
82             lazy => 1,
83             isa => q{ArrayRef},
84             default => sub {
85             my $self = shift;
86             my @file_list = ();
87             my $wanted = sub {
88             my $filename = $File::Find::name;
89             for my $extension (@{$self->allowed_extensions}) {
90             if (-f and /^[^\.].*\.$extension$/) {
91             push @file_list, $filename;
92             }
93             }
94             };
95             my $root = $self->file_root;
96             my @files = scalar(@{ $self->filenames() }) ? @{ $self->filenames() } : ($root);
97             find($wanted, @files);
98             return \@file_list;
99             }
100             );
101              
102             has files => (
103             is => q{ro},
104             required => 1,
105             lazy => 1,
106             default => sub {
107             my $self = shift;
108             my @files = ();
109             for my $filename ( @{ $self->file_paths } ) {
110             push @files, Test::Story::File->new({
111             filename => $filename,
112             config => dclone( $self->config ),
113             });
114             }
115             return \@files;
116             }
117             );
118              
119             sub run_tests {
120             my $self = shift;
121             my $id = $self->testcase_id;
122             my $suite = Test::FITesque::Suite->new();
123              
124             my $test_count = 0;
125             foreach my $file (@{ $self->files }) {
126             my @cases = @{ $file->filtered_cases( $id ) };
127             foreach my $case (@cases) {
128             my @data = @{ $case->test_data };
129             my $test = Test::FITesque::Test->new({
130             data => [
131             [$file->fixture_class, { testcase => $case } ],
132             @data
133             ]
134             });
135             $suite->add($test);
136             $test_count++;
137             }
138             }
139             $suite->run_tests() if $test_count > 0;
140             }
141              
142             # unimport moose functions to make immutable
143             no Moose;
144             __PACKAGE__->meta->make_immutable();
145             1;
146              
147             =pod
148              
149             =head1 NAME
150              
151             Test::Story - Storytest Automation Runner
152              
153             =head1 SYNOPSIS
154              
155             my $a8 = Test::Story->new({
156             filenames => [qw( cases/test1.tc )],
157             file_root => 'cases',
158             });
159             $a8->run_tests();
160              
161             =head1 DESCRIPTION
162              
163             Test::Story was created as a mechanism for writing and running automated
164             storytests in a human-readable and reusable fashion. Its storytest files
165             are easily readable, are natural to write, and are easy for non-technical
166             users to read, while still being easy for developers to automate.
167              
168             It works by leveraging L<Test::FITesque> to describe test fixtures in a
169             list style, while providing syntatic sugar for test authors. The tests
170             themselves are written in YAML, and while they have a specific
171             structure, it doesn't limit test author's flexibility. And many of the
172             features of YAML, most notably its concept of creating pointers between
173             different parts of a document, means reusing parts of your tests for use
174             elsewhere is trivial.
175              
176             =head2 Testcase Syntax
177              
178             An Story testcase file can consist of mulitple YAML documents, separated by
179             three dashes C<--->. But the structure for each testcase within a file
180             is the same. It consists of a name, a summary, an optional ID, test
181             instructions, and optional preconditions and postconditions.
182              
183             The instructions, preconditions, and postconditions contain a list of steps
184             that are to be run as part of the test.
185              
186             ---
187             NAME: Administrator changes their timezone
188             ID: admin_changes_tz
189             SUMMARY: Administrators need to have a mechanism for changing their
190             timezone.
191             PRECONDITIONS:
192             - ensure user exists: admin
193             - ensure timezone is: America/Vancouver
194             INSTRUCTIONS:
195             - login:
196             username: admin
197             password: testpass
198             - goto page: Account Settings
199             - verify current timezone is: America/Vancouver
200             - change timezone to: Australia/Brisbane
201             - verify current timezone is: Australia/Brisbane
202             POSTCONDITIONS:
203             - ensure timezone is: America/Vancouver
204              
205             Despite the actual order specified in a testcase block, each testcase will
206             run its tests in the order of 1) precondition, 2) instructions, 3)
207             postconditions. If you don't specify an ID, then one will be
208             auto-generated for you based on the testcase name.
209              
210             =head2 Goals of this Testcase Format
211              
212             When a product manager or some other non-technical business user comes up
213             with a set of initial story tests for some feature, most people will
214             naturally think of writing a list. Even describing how to access some
215             feature, people will fall back to describing a list of steps. We therefore
216             set out to capture that as closely as possible in our tests.
217              
218             As much as possible, we recommend making your fixture calls, and their
219             arguments, read as much as possible as english phrases, without having
220             unnecessary "no-op" filler that will get in the way.
221              
222             =head2 Testcase Fixtures and Suggestions
223              
224             From the sample testcase described at the beginning of this section, you
225             can see just how natural this method of writing tests is. The tests don't
226             even need to be automated to be able to follow them, as they're readable
227             enough for a user to navigate a website or command-line to be able to
228             follow the specified steps.
229              
230             However, once you do automate them, there are some subtleties that we've
231             found work quite well in authoring test cases. Namely in there's a
232             difference between performing an action, and testing the result. For
233             instance:
234              
235             - change timezone to: <some timezone>
236             - verify timezone is: <some timezone>
237             - ensure timezone is: <some timezone>
238              
239             "change" can be used to set some value using the UI you're testing.
240             "verify" can be used to test that some value is set or is present.
241             "ensure" is a subtle one, but it can be used to change a value only if it
242             isn't already present. It's useful to use in preconditions where you don't
243             care to exercise the UI every time you need to add a user, for instance.
244             If you need a user account simply for testing purposes, then your C<ensure
245             user exists> action can simply drop a user account into a database or onto
246             disk without using the UI. This not only speeds up your tests, but makes
247             sure that you only test user creation within your user account tests, not
248             on every test that requires a user to be present.
249              
250             =head1 METHODS
251              
252             =head2 Accessors
253              
254             =over 4
255              
256             =item fixture_base
257              
258             Specifies the base fixture classname to use when running testcases. When
259             test files are found within sub-directories of L</file_root>, the directory
260             names are converted to class names, and appended to this L</fixture_base>
261             value.
262              
263             =item file_root
264              
265             Indicates where your testcase files live. This is important because
266             any directory below this point is assumed to be part of the fixture class
267             name.
268              
269             =item filenames
270              
271             The list of test files you wish L<Test::Story> to run. If none are
272             specified, the test runner will try to find all files under the
273             L</file_root> that has an extension in the L</allowed_extensions> list.
274              
275             =item verbose
276              
277             Turns on increasing amounts of debugging output. All debug messages are
278             prefixed with a "#" so that it doesn't interfere with TAP output.
279              
280             Default: 0
281              
282             =item allowed_extensions
283              
284             Specifies what file extensions are valid testcases. In this way you can
285             mix story tests and unit tests within the same directory.
286              
287             Default: "st", "tc"
288              
289             =item testcase_id
290              
291             Specifies a testcase ID you wish to run. If unset, it will run all
292             testcases in test files.
293              
294             =back
295              
296             =head2 Object Methods
297              
298             =over 4
299              
300             =item run_tests
301              
302             Calls L<Test::Story::File/run_tests>() in all the L<Test::Story::File>
303             objects returned by L</files>.
304              
305             =item file_paths
306              
307             Returns a list of paths to all the testcase files that are to be processed.
308             If nothing is specified in L</filenames>, or it contains directories, then
309             L</file_paths> will search in those sub-directories to find any available
310             testcase files.
311              
312             =item files
313              
314             Returns the contents of L</file_paths> as instances of the
315             L<Test::Story::File> class.
316              
317             =back
318              
319             =head1 SEE ALSO
320              
321             L<Test::Story::File>, L<Test::FITesque>
322              
323             =head1 SOURCE CONTROL
324              
325             http://github.com/NachoMan/Test-Story
326              
327             =head1 AUTHORS
328              
329             Michael Nachbaur E<lt>mike@nachbaur.comE<gt>,
330             Scott McWhirter E<lt>konobi@cpan.orgE<gt>
331              
332             =head1 LICENSE
333              
334             This library is free software; you can redistribute it and/or modify
335             it under the same terms as Perl itself, either Perl version 5.8.7 or,
336             at your option, any later version of Perl 5 you may have available.
337              
338             =head1 COPYRIGHT
339              
340             Copyright (C) 2008 Sophos, Plc.
341              
342             =cut