File Coverage

blib/lib/Test/Rest.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Test::Rest;
2 1     1   131111 use strict;
  1         2  
  1         34  
3 1     1   4 use warnings;
  1         1  
  1         25  
4 1     1   5 use Carp;
  1         5  
  1         81  
5 1     1   348 use XML::LibXML;
  0            
  0            
6             use Test::Rest::Commands;
7             use Test::Rest::Context;
8             use URI;
9             use Test::More;
10             use Data::Dumper;
11              
12             our $VERSION = '0.03';
13              
14             =head1 NAME
15              
16             Test::Rest - Declarative test framework for RESTful web services
17              
18             =head1 SYNOPSIS
19              
20             This module is very experimental/alpha and will likely change. It's not super usable at the moment, but I'm open to feedback and suggestions on how to move forward, and feature requests are OK too.
21              
22             use Test::Rest;
23              
24             # Scan the directory './tests' for test declaration files
25             # and run them against the server http://webservice.example.com/
26             # e.g.
27             # ./tests/01-authentication.xml
28             # ./tests/02-create-a-foobar.xml
29             # ./tests/03-delete-a-foobar.xml
30             my $tests = Test::Rest->new(dir => 'tests', base => 'http://webservice.example.com/');
31             $tests->run;
32              
33             =head1 DESCRIPTION
34              
35             The idea here is to write tests against REST services in a data-driven, declarative way.
36              
37             Here is an example test description file:
38              
39            
40             user/login
41            
42            
43             myname
44             mypass
45            
46            
47             200
48            
49            
50            
51            
52            
53            
54             Testy
55             McTester
56             {mail}
57             {pass}
58            
59            
60            
61             200
62            
63             Created {uid}
64            
65              
66             =over
67              
68             Things to note:
69              
70             =item *
71              
72             Each child of the top-level element represents a command or test, and they are executed sequentially by Test::Rest.
73              
74             =item *
75              
76             Methods like 'get', 'post', and 'submit_form' map to the equivalent methods of L or L - they result in a request being made to the server.
77              
78             =item *
79              
80             The default user agent is L. Cookies/sessions are stored between requests, and are kept for current test file.
81              
82             =item *
83              
84             The web service URLs given are relative paths and are automatically prefixed by the 'base' parameter given to new().
85              
86             =item *
87              
88             Template::Toolkit is used to expand template variables. The template stash (variable hash) persists until the end of the test file. The 'set' command can be used to add variables to the stash.
89              
90             =item *
91              
92             The most recent L is stored in the stash via the key 'response'. If the response type is an XML document, the response document is automatically parsed and available to future tests/commands via XPath, and via the stash key 'document'. The whole history of responses and documents are available via the stash keys 'responses' and 'documents' respectively.
93              
94             =item *
95              
96             A jQuery/XPath-like template variable syntax is available for referencing parts of the last received document. E.g. to see the href of the first anchor tag, you would use $(a[1]/@href)
97              
98             =back
99              
100             =head1 COMMANDS
101              
102             TODO
103              
104             =cut
105              
106             use vars qw/$file $line $where/;
107              
108             sub new {
109             my $proto = shift;
110             my $class = ref $proto || $proto;
111             my %opts = @_;
112             return bless \%opts, $class;
113             }
114              
115             sub run {
116             my $self = shift;
117             croak 'Parameter "base_url" required' unless defined $self->{base_url};
118             $self->{base_url} = URI->new($self->{base_url});
119             if (defined $self->{dir}) {
120             my $dir = $self->{dir};
121             croak "Directory $dir not found" unless -d $dir;
122             opendir(my $dh, $dir) || croak "can't opendir $dir: $!";
123             while (my $t = readdir($dh)) {
124             next unless $t =~ /\.xml$/;
125             $self->run_test_file("$dir/$t");
126             }
127             closedir $dh;
128             }
129             elsif (defined $self->{files}) {
130             foreach (@{$self->{files}}) {
131             croak "$_ not found" unless -f $_;
132             $self->run_test_file($_);
133             }
134             }
135             done_testing();
136             }
137              
138             sub run_test_file {
139             my $self = shift;
140             my $filename = shift;
141             $Test::Rest::file = $filename;
142             my $doc = XML::LibXML->load_xml(location => $filename, line_numbers => 1);
143             diag("Loaded $filename");
144             my $commands = Test::Rest::Commands->new;
145             my $context = Test::Rest::Context->new(tests => $doc, base_url => $self->{base_url}, stash => {%{$self->{stash}}});
146             foreach my $child ($doc->documentElement->childNodes) {
147             next unless $child->nodeType == XML_ELEMENT_NODE;
148             my $cmd = $child->localname;
149             $Test::Rest::line = $child->line_number;
150             $Test::Rest::where = "at $Test::Rest::file line $Test::Rest::line";
151             croak "Unsupported command '$cmd' $Test::Rest::where" unless $commands->can($cmd);
152             $context->test($child);
153             $commands->$cmd($context);
154             }
155             }
156              
157             =head1 AUTHOR
158              
159             Keith Grennan, C<< >>
160              
161             =head1 TODO
162              
163             =over
164              
165             =item *
166              
167             This initial implementation is very XML/XPath-centric, but there's certainly room to incorporate other formats (YAML, JSON, etc)
168              
169             =item *
170              
171             Figure out how to make friendly with Test::Harness and whatnot
172              
173             =item *
174              
175             Allow extensions to supply custom commands, tests, formats
176              
177             =back
178              
179             =head1 SEE ALSO
180              
181             L, L, L