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 |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
=head1 BUGS |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
Please report any bugs or feature requests to C, or through |
186
|
|
|
|
|
|
|
the web interface at L. I will be notified, and then you'll |
187
|
|
|
|
|
|
|
automatically be notified of progress on your bug as I make changes. |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
=head1 SUPPORT |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
You can find documentation for this module with the perldoc command. |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
perldoc Test::Rest |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
You can also look for information at: |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
=over 4 |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
=item * RT: CPAN's request tracker |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
L |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
=item * AnnoCPAN: Annotated CPAN documentation |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
L |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
=item * CPAN Ratings |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
L |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
=item * Search CPAN |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
L |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
=back |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
=head1 COPYRIGHT & LICENSE |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
Copyright 2010 Keith Grennan, all rights reserved. |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
222
|
|
|
|
|
|
|
under the same terms as Perl itself. |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
=cut |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
1; # End of Test::Rest |