Make default test names reflect reality more and fix a buglet, rafl++
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Test.pm
CommitLineData
fc7ec1d9 1package Catalyst::Test;
2
d9d04ded 3use strict;
4use warnings;
b474372a 5use Test::More ();
e8d0f69a 6
a2f2cde9 7use Catalyst::Exception;
d837e1a7 8use Catalyst::Utils;
7dd4f037 9use Class::MOP;
87cbe5e6 10use Sub::Exporter;
11
e0a78010 12my $build_exports = sub {
87cbe5e6 13 my ($self, $meth, $args, $defaults) = @_;
e8d0f69a 14
87cbe5e6 15 my $request;
16 my $class = $args->{class};
17
18 if ( $ENV{CATALYST_SERVER} ) {
19 $request = sub { remote_request(@_) };
20 } elsif (! $class) {
21 $request = sub { Catalyst::Exception->throw("Must specify a test app: use Catalyst::Test 'TestApp'") };
22 } else {
7dd4f037 23 unless (Class::MOP::is_class_loaded($class)) {
24 Class::MOP::load_class($class);
87cbe5e6 25 }
26 $class->import;
27
28 $request = sub { local_request( $class, @_ ) };
29 }
30
31 my $get = sub { $request->(@_)->content };
32
4fbc0e85 33 my $ctx_request = sub {
702729f5 34 my $me = ref $self || $self;
269194b4 35
4fbc0e85 36 ### throw an exception if ctx_request is being used against a remote
269194b4 37 ### server
38 Catalyst::Exception->throw("$me only works with local requests, not remote")
39 if $ENV{CATALYST_SERVER};
40
ba151d0d 41 ### check explicitly for the class here, or the Cat->meta call will blow
42 ### up in our face
43 Catalyst::Exception->throw("Must specify a test app: use Catalyst::Test 'TestApp'") unless $class;
44
269194b4 45 ### place holder for $c after the request finishes; reset every time
46 ### requests are done.
7f891e1e 47 my $ctx_closed_over;
269194b4 48
49 ### hook into 'dispatch' -- the function gets called after all plugins
50 ### have done their work, and it's an easy place to capture $c.
269194b4 51
cf1fb734 52 my $meta = Class::MOP::get_metaclass_by_name($class);
ba151d0d 53 $meta->make_mutable;
54 $meta->add_after_method_modifier( "dispatch", sub {
7f891e1e 55 $ctx_closed_over = shift;
ba151d0d 56 });
cf1fb734 57 $meta->make_immutable( replace_constructor => 1 );
94f74acd 58 Class::C3::reinitialize(); # Fixes RT#46459, I've failed to write a test for how/why, but it does.
269194b4 59 ### do the request; C::T::request will know about the class name, and
60 ### we've already stopped it from doing remote requests above.
61 my $res = $request->( @_ );
62
7f891e1e 63 # Make sure not to leave a reference $ctx hanging around.
64 # This means that the context will go out of scope as soon as the
65 # caller disposes of it, rather than waiting till the next time
66 # that ctx_request is called. This can be important if your $ctx
67 # ends up with a reference to a shared resource or lock (for example)
68 # which you want to clean up in test teardown - if the $ctx is still
69 # closed over then you're stuffed...
70 my $ctx = $ctx_closed_over;
71 undef $ctx_closed_over;
72
269194b4 73 ### return both values
7f891e1e 74 return ( $res, $ctx );
269194b4 75 };
76
87cbe5e6 77 return {
4fbc0e85 78 request => $request,
79 get => $get,
80 ctx_request => $ctx_request,
87cbe5e6 81 content_like => sub {
82 my $action = shift;
83 return Test::More->builder->like($get->($action),@_);
84 },
85 action_ok => sub {
86 my $action = shift;
901991e6 87 my $meth = $request->($action)->request->method;
88 my @args = @_ ? @_ : ("$meth $action returns successfully");
89 return Test::More->builder->ok($request->($action)->is_success,@args);
87cbe5e6 90 },
91 action_redirect => sub {
92 my $action = shift;
901991e6 93 my $meth = $request->($action)->request->method;
94 my @args = @_ ? @_ : ("$meth $action returns a redirect");
95 return Test::More->builder->ok($request->($action)->is_redirect,@args);
87cbe5e6 96 },
97 action_notfound => sub {
98 my $action = shift;
901991e6 99 my $meth = $request->($action)->request->method;
100 my @args = @_ ? @_ : ("$meth $action returns a 404");
101 return Test::More->builder->is_eq($request->($action)->code,404,@args);
87cbe5e6 102 },
103 contenttype_is => sub {
104 my $action = shift;
105 my $res = $request->($action);
106 return Test::More->builder->is_eq(scalar($res->content_type),@_);
107 },
108 };
e0a78010 109};
e8d0f69a 110
d9d04ded 111our $default_host;
6e6df63d 112
113{
114 my $import = Sub::Exporter::build_exporter({
e0a78010 115 groups => [ all => $build_exports ],
6e6df63d 116 into_level => 1,
117 });
118
d9d04ded 119
6e6df63d 120 sub import {
d9d04ded 121 my ($self, $class, $opts) = @_;
955d6da6 122 Carp::carp(
123qq{Importing Catalyst::Test without an application name is deprecated:\n
124Instead of saying: use Catalyst::Test;
125say: use Catalyst::Test (); # If you don't want to import a test app right now.
126or say: use Catalyst::Test 'MyApp'; # If you do want to import a test app.\n\n})
127 unless $class;
6e6df63d 128 $import->($self, '-all' => { class => $class });
d258fcb2 129 $opts = {} unless ref $opts eq 'HASH';
d9d04ded 130 $default_host = $opts->{default_host} if exists $opts->{default_host};
269194b4 131 return 1;
6e6df63d 132 }
133}
134
fc7ec1d9 135=head1 NAME
136
8d2fa70c 137Catalyst::Test - Test Catalyst Applications
fc7ec1d9 138
139=head1 SYNOPSIS
140
49faa307 141 # Helper
49faa307 142 script/test.pl
143
fc7ec1d9 144 # Tests
145 use Catalyst::Test 'TestApp';
26dd6d9f 146 my $content = get('index.html'); # Content as string
147 my $response = request('index.html'); # HTTP::Response object
4fbc0e85 148 my($res, $c) = ctx_request('index.html'); # HTTP::Response & context object
fc7ec1d9 149
2f381252 150 use HTTP::Request::Common;
151 my $response = request POST '/foo', [
152 bar => 'baz',
153 something => 'else'
154 ];
155
45374ac6 156 # Run tests against a remote server
21465c88 157 CATALYST_SERVER='http://localhost:3000/' prove -r -l lib/ t/
45374ac6 158
b6898a9f 159 use Catalyst::Test 'TestApp';
e8d0f69a 160 use Test::More tests => 1;
b6898a9f 161
162 ok( get('/foo') =~ /bar/ );
163
d9d04ded 164 # mock virtual hosts
165 use Catalyst::Test 'MyApp', { default_host => 'myapp.com' };
166 like( get('/whichhost'), qr/served by myapp.com/ );
167 like( get( '/whichhost', { host => 'yourapp.com' } ), qr/served by yourapp.com/ );
168 {
169 local $Catalyst::Test::default_host = 'otherapp.com';
170 like( get('/whichhost'), qr/served by otherapp.com/ );
171 }
172
fc7ec1d9 173=head1 DESCRIPTION
174
2f381252 175This module allows you to make requests to a Catalyst application either without
176a server, by simulating the environment of an HTTP request using
177L<HTTP::Request::AsCGI> or remotely if you define the CATALYST_SERVER
0eb98177 178environment variable. This module also adds a few Catalyst-specific
179testing methods as displayed in the method section.
2f381252 180
f98f669b 181The L<get|/"$content = get( ... )"> and L<request|/"$res = request( ... );">
182functions take either a URI or an L<HTTP::Request> object.
fc7ec1d9 183
5f2e949d 184=head1 INLINE TESTS WILL NO LONGER WORK
185
186While it used to be possible to inline a whole testapp into a C<.t> file for a
187distribution, this will no longer work.
188
189The convention is to place your L<Catalyst> test apps into C<t/lib> in your
190distribution. E.g.: C<t/lib/TestApp.pm>, C<t/lib/TestApp/Controller/Root.pm>,
191etc.. Multiple test apps can be used in this way.
192
193Then write your C<.t> files like so:
194
195 use strict;
196 use warnings;
197 use FindBin '$Bin';
198 use lib "$Bin/lib";
199 use Test::More tests => 6;
200 use Catalyst::Test 'TestApp';
201
03f7a71b 202=head1 METHODS
fc7ec1d9 203
26dd6d9f 204=head2 $content = get( ... )
fc7ec1d9 205
206Returns the content.
207
208 my $content = get('foo/bar?test=1');
209
f13fc03f 210Note that this method doesn't follow redirects, so to test for a
211correctly redirecting page you'll need to use a combination of this
f98f669b 212method and the L<request|/"$res = request( ... );"> method below:
f13fc03f 213
214 my $res = request('/'); # redirects to /y
215 warn $res->header('location');
216 use URI;
217 my $uri = URI->new($res->header('location'));
218 is ( $uri->path , '/y');
219 my $content = get($uri->path);
220
8fa9321c 221Note also that the content is returned as raw bytes, without any attempt
222to decode it into characters.
223
26dd6d9f 224=head2 $res = request( ... );
fc7ec1d9 225
0eb98177 226Returns an L<HTTP::Response> object. Accepts an optional hashref for request
d9d04ded 227header configuration; currently only supports setting 'host' value.
fc7ec1d9 228
795117cf 229 my $res = request('foo/bar?test=1');
d9d04ded 230 my $virtual_res = request('foo/bar?test=1', {host => 'virtualhost.com'});
fc7ec1d9 231
26dd6d9f 232=head1 FUNCTIONS
233
f2e13bbd 234=head2 ($res, $c) = ctx_request( ... );
26dd6d9f 235
f98f669b 236Works exactly like L<request|/"$res = request( ... );">, except it also returns the Catalyst context object,
51a75afc 237C<$c>. Note that this only works for local requests.
26dd6d9f 238
26dd6d9f 239=head2 $res = Catalyst::Test::local_request( $AppClass, $url );
0f895006 240
2f381252 241Simulate a request using L<HTTP::Request::AsCGI>.
242
0f895006 243=cut
244
245sub local_request {
246 my $class = shift;
247
248 require HTTP::Request::AsCGI;
249
250 my $request = Catalyst::Utils::request( shift(@_) );
d9d04ded 251 _customize_request($request, @_);
0f895006 252 my $cgi = HTTP::Request::AsCGI->new( $request, %ENV )->setup;
253
e8674cf7 254 $class->handle_request( env => \%ENV );
0f895006 255
12755afc 256 my $response = $cgi->restore->response;
257 $response->request( $request );
f23f1634 258
259 # HTML head parsing based on LWP::UserAgent
260
261 require HTML::HeadParser;
262
263 my $parser = HTML::HeadParser->new();
264 $parser->xml_mode(1) if $response->content_is_xhtml;
265 $parser->utf8_mode(1) if $] >= 5.008 && $HTML::Parser::VERSION >= 3.40;
266
267 $parser->parse( $response->content );
268 my $h = $parser->header;
269 for my $f ( $h->header_field_names ) {
270 $response->init_header( $f, [ $h->header($f) ] );
271 }
272
12755afc 273 return $response;
0f895006 274}
275
523d44ec 276my $agent;
277
26dd6d9f 278=head2 $res = Catalyst::Test::remote_request( $url );
bea4160a 279
b77e7869 280Do an actual remote request using LWP.
bea4160a 281
282=cut
283
45374ac6 284sub remote_request {
45374ac6 285
68eb5874 286 require LWP::UserAgent;
287
d837e1a7 288 my $request = Catalyst::Utils::request( shift(@_) );
0f895006 289 my $server = URI->new( $ENV{CATALYST_SERVER} );
523d44ec 290
d9d04ded 291 _customize_request($request, @_);
292
523d44ec 293 if ( $server->path =~ m|^(.+)?/$| ) {
890e8d18 294 my $path = $1;
295 $server->path("$path") if $path; # need to be quoted
f4c0f6f7 296 }
cdae055a 297
298 # the request path needs to be sanitised if $server is using a
299 # non-root path due to potential overlap between request path and
300 # response path.
301 if ($server->path) {
f4c0f6f7 302 # If request path is '/', we have to add a trailing slash to the
303 # final request URI
304 my $add_trailing = $request->uri->path eq '/';
0eb98177 305
cdae055a 306 my @sp = split '/', $server->path;
307 my @rp = split '/', $request->uri->path;
308 shift @sp;shift @rp; # leading /
309 if (@rp) {
310 foreach my $sp (@sp) {
a7daf37e 311 $sp eq $rp[0] ? shift @rp : last
cdae055a 312 }
313 }
314 $request->uri->path(join '/', @rp);
0eb98177 315
f4c0f6f7 316 if ( $add_trailing ) {
317 $request->uri->path( $request->uri->path . '/' );
318 }
523d44ec 319 }
320
321 $request->uri->scheme( $server->scheme );
322 $request->uri->host( $server->host );
323 $request->uri->port( $server->port );
324 $request->uri->path( $server->path . $request->uri->path );
325
68eb5874 326 unless ($agent) {
9ffadf88 327
d837e1a7 328 $agent = LWP::UserAgent->new(
523d44ec 329 keep_alive => 1,
330 max_redirect => 0,
331 timeout => 60,
0eb98177 332
d11e0c1d 333 # work around newer LWP max_redirect 0 bug
334 # http://rt.cpan.org/Ticket/Display.html?id=40260
335 requests_redirectable => [],
523d44ec 336 );
d837e1a7 337
523d44ec 338 $agent->env_proxy;
339 }
45374ac6 340
341 return $agent->request($request);
fc7ec1d9 342}
343
d9d04ded 344sub _customize_request {
345 my $request = shift;
346 my $opts = pop(@_) || {};
4348c28b 347 $opts = {} unless ref($opts) eq 'HASH';
d9d04ded 348 if ( my $host = exists $opts->{host} ? $opts->{host} : $default_host ) {
349 $request->header( 'Host' => $host );
350 }
351}
352
0bf3b41c 353=head2 action_ok($url [, $test_name ])
e8d0f69a 354
0bf3b41c 355Fetches the given URL and checks that the request was successful. An optional
356second argument can be given to specify the name of the test.
e8d0f69a 357
0bf3b41c 358=head2 action_redirect($url [, $test_name ])
e8d0f69a 359
0bf3b41c 360Fetches the given URL and checks that the request was a redirect. An optional
361second argument can be given to specify the name of the test.
e8d0f69a 362
0bf3b41c 363=head2 action_notfound($url [, $test_name ])
e8d0f69a 364
0bf3b41c 365Fetches the given URL and checks that the request was not found. An optional
366second argument can be given to specify the name of the test.
0eb98177 367
0bf3b41c 368=head2 content_like( $url, $regexp [, $test_name ] )
e8d0f69a 369
0bf3b41c 370Fetches the given URL and returns whether the content matches the regexp. An
371optional third argument can be given to specify the name of the test.
e8d0f69a 372
0bf3b41c 373=head2 contenttype_is($url, $type [, $test_name ])
e8d0f69a 374
0bf3b41c 375Verify the given URL has a content type of $type and optionally specify a test name.
e8d0f69a 376
fc7ec1d9 377=head1 SEE ALSO
378
2f381252 379L<Catalyst>, L<Test::WWW::Mechanize::Catalyst>,
380L<Test::WWW::Selenium::Catalyst>, L<Test::More>, L<HTTP::Request::Common>
fc7ec1d9 381
2f381252 382=head1 AUTHORS
fc7ec1d9 383
2f381252 384Catalyst Contributors, see Catalyst.pm
fc7ec1d9 385
386=head1 COPYRIGHT
387
536bee89 388This library is free software. You can redistribute it and/or modify it under
fc7ec1d9 389the same terms as Perl itself.
390
391=cut
392
3931;