/
Exception.pm
executable file
·333 lines (214 loc) · 8.52 KB
/
Exception.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#! /usr/bin/perl -w
package Test::Exception;
use 5.005;
use strict;
use Test::Builder;
use Sub::Uplevel;
use base qw(Exporter);
use vars qw($VERSION @EXPORT @EXPORT_OK);
$VERSION = '0.18';
@EXPORT = qw(dies_ok lives_ok throws_ok lives_and);
my $Tester = Test::Builder->new;
sub import {
my $self = shift;
if (@_) {
my $package = caller;
$Tester->exported_to($package);
$Tester->plan(@_);
};
$self->export_to_level(1, $self, $_) foreach @EXPORT;
}
=head1 NAME
Test::Exception - Test exception based code
=head1 SYNOPSIS
use Test::More tests => 5;
use Test::Exception;
# or if you don't need Test::More
use Test::Exception tests => 5;
# then...
# Check that something died
dies_ok {$foo->method1} 'expecting to die';
# Check that something did not die
lives_ok {$foo->method2} 'expecting to live';
# Check that the stringified exception matches given regex
throws_ok {$foo->method3} qr/division by zero/, 'zero caught okay';
# Check an exception of the given class (or subclass) is thrown
throws_ok {$foo->method4} 'Error::Simple', 'simple error thrown';
# Check that a test runs without an exception
lives_and {is $foo->method, 42} 'method is 42';
=head1 DESCRIPTION
This module provides a few convenience methods for testing exception based code. It is built with L<Test::Builder> and plays happily with L<Test::More> and friends.
If you are not already familiar with L<Test::More> now would be the time to go take a look.
You can specify the test plan when you C<use Test::Exception> in the same way as C<use Test::More>. See L<Test::More> for details.
=cut
sub _try_as_caller {
my $coderef = shift;
eval { uplevel 3, $coderef };
return $@;
};
sub _is_exception {
my $exception = shift;
ref($exception) || $exception ne '';
};
sub _exception_as_string {
my ($prefix, $exception) = @_;
return "$prefix undef" unless defined($exception);
return "$prefix normal exit" unless _is_exception($exception);
my $class = ref($exception);
$exception = "$class ($exception)"
if $class && "$exception" !~ m/^\Q$class/;
chomp($exception);
return("$prefix $exception");
};
=over 4
=item B<dies_ok>
Checks that a piece of code dies, rather than returning normally. For example:
sub div {
my ($a, $b) = @_;
return( $a / $b );
};
dies_ok { div(1, 0) } 'divide by zero detected';
A true value is returned if the test succeeds, false otherwise. On exit $@ is guaranteed to be the cause of death (if any).
The test name is optional, but recommended.
=cut
sub dies_ok (&;$) {
my ($coderef, $name) = @_;
my $exception = _try_as_caller($coderef);
my $ok = $Tester->ok( _is_exception($exception), $name );
$@ = $exception;
return($ok);
}
=item B<lives_ok>
Checks that a piece of code exits normally, and doesn't die. For example:
sub read_file {
my $file = shift;
local $/ = undef;
open(FILE, $file) or die "open failed ($!)\n";
$file = <FILE>;
close(FILE);
return($file);
};
my $file;
lives_ok { $file = read_file('test.txt') } 'file read';
Should a lives_ok() test fail it produces appropriate diagnostic messages. For example:
not ok 1 - file read
# Failed test (test.t at line 15)
# died: open failed (No such file or directory)
A true value is returned if the test succeeds, false otherwise. On exit $@ is guaranteed to be the cause of death (if any).
The test name is optional, but recommended.
=cut
sub lives_ok (&;$) {
my ($coderef, $name) = @_;
my $exception = _try_as_caller($coderef);
my $ok = $Tester->ok(! _is_exception($exception), $name)
|| $Tester->diag(_exception_as_string("died:", $exception));
$@ = $exception;
return($ok);
}
=item B<throws_ok>
Tests to see that a specific exception is thrown. throws_ok() has two forms:
throws_ok BLOCK REGEX, TEST_NAME
throws_ok BLOCK CLASS, TEST_NAME
In the first form the test passes if the stringified exception matches the give regular expression. For example:
throws_ok {
read_file('test.txt')
} qr/No such file/, 'no file';
If your perl does not support C<qr//> you can also pass a regex-like string, for example:
throws_ok {
read_file('/etc/kcpassword')
} '/Permission denied/', 'no permissions';
The second form of throws_ok() test passes if the exception is of the same class as the one supplied, or a subclass of that class. For example:
throws_ok {$foo->bar} "Error::Simple", 'simple error';
Will only pass if the C<bar> method throws an Error::Simple exception, or a subclass of an Error::Simple exception.
You can get the same effect by passing an instance of the exception you want to look for. The following is equivalent to the previous example:
my $SIMPLE = Error::Simple->new();
throws_ok {$foo->bar} $SIMPLE, 'simple error';
Should a throws_ok() test fail it produces appropriate diagnostic messages. For example:
not ok 3 - simple error
# Failed test (test.t at line 48)
# expecting: Error::Simple exception
# found: normal exit
A true value is returned if the test succeeds, false otherwise. On exit $@ is guaranteed to be the cause of death (if any).
The test name is optional. If no test name is given a description of the exception being checked for is used.
=cut
sub throws_ok (&$;$) {
my ($coderef, $expecting, $name) = @_;
$name ||= _exception_as_string("threw", $expecting);
my $exception = _try_as_caller($coderef);
my $regex = $Tester->maybe_regex($expecting);
my $ok = $regex ? ($exception =~ m/$regex/)
: UNIVERSAL::isa($exception, ref($expecting) || $expecting);
$Tester->ok($ok, $name);
unless ($ok) {
$Tester->diag( _exception_as_string("expecting:", $expecting) );
$Tester->diag( _exception_as_string("found:", $exception) );
};
$@ = $exception;
return($ok);
};
=item B<lives_and>
Run a test that may throw an exception. For example, instead of doing:
my $file;
lives_ok { $file = read_file('answer.txt') } 'read_file worked';
is $file, "42\n", 'answer was 42';
You can use lives_and() like this:
lives_and { is read_file('answer.txt'), "42\n" } 'answer is 42';
Which is the same as doing
is read_file('answer.txt'), "42\n", 'answer is 42';
unless C<read_file('answer.txt')> dies, in which case you get the same kind of error as lives_ok()
not ok 1 - answer is 42
# Failed test (test.t at line 15)
# died: open failed (No such file or directory)
A true value is returned if the test succeeds, false otherwise. On exit $@ is guaranteed to be the cause of death (if any).
The test name is optional, but recommended.
=cut
sub lives_and (&$) {
my ($test, $name) = @_;
local $Test::Builder::Level = $Test::Builder::Level+1;
{
my $ok = \&Test::Builder::ok;
no warnings;
local *Test::Builder::ok = sub {
$_[2] = $name unless defined $_[2];
$ok->(@_);
};
use warnings;
eval { $test->() } and return 1;
};
my $exception = $@;
if (_is_exception($exception)) {
$Tester->ok(0, $name);
$Tester->diag( _exception_as_string("died:", $exception) );
};
$@ = $exception;
return;
}
=back
=head1 BUGS
None known at the time of writing.
If you find any please let me know by e-mail, or report the problem with L<http://rt.cpan.org/>.
=head1 TO DO
Nothing at the time of writing.
If you think this module should do something that it doesn't do at the moment please let me know.
=head1 ACKNOWLEGEMENTS
Thanks to chromatic and Michael G Schwern for the excellent Test::Builder, without which this module wouldn't be possible.
Thanks to Michael G Schwern, Mark Fowler, Janek Schleicher, chromatic, Mark Fowler, Peter Scott, Aristotle and Andy Lester for suggestions, bug reports and patches.
=head1 AUTHOR
Adrian Howard <adrianh@quietstars.com>
If you can spare the time, please drop me a line if you find this module useful.
=head1 SEE ALSO
=over 4
=item L<Test::Builder>
Support module for building test libraries.
=item L<Test::Simple> & L<Test::More>
Basic utilities for writing tests.
=item L<Test::Warn> & L<Test::NoWarnings>
Modules to help test warnings.
=item L<http://qa.perl.org/test-modules.html>
Overview of some of the many testing modules available on CPAN.
=back
=head1 LICENCE
Copyright 2002-2004 Adrian Howard, All Rights Reserved.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
=cut
1;