File Coverage

blib/lib/Test/A8N.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


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