1 |
|
---|
2 | =encoding utf8
|
---|
3 |
|
---|
4 | =head1 NAME
|
---|
5 |
|
---|
6 | Mojolicious::Guides::Testing - Web Application Testing Made Easy
|
---|
7 |
|
---|
8 | =head1 OVERVIEW
|
---|
9 |
|
---|
10 | This document is an introduction to testing web applications with L<Test::Mojo>.
|
---|
11 | L<Test::Mojo> can be thought of as a module that provides all of the tools and
|
---|
12 | testing assertions needed to test web applications in a Perl-ish way.
|
---|
13 |
|
---|
14 | While L<Test::Mojo> can be used to test any web application, it has shortcuts
|
---|
15 | designed to make testing L<Mojolicious> web applications easy and pain-free.
|
---|
16 |
|
---|
17 | Please refer to the L<Test::Mojo> documentation for a complete reference to many
|
---|
18 | of the ideas and syntax introduced in this document.
|
---|
19 |
|
---|
20 | A test file for a simple web application might look like:
|
---|
21 |
|
---|
22 | use Mojo::Base -strict;
|
---|
23 |
|
---|
24 | use Test::Mojo;
|
---|
25 | use Test::More;
|
---|
26 |
|
---|
27 | # Start a Mojolicious app named "Celestial"
|
---|
28 | my $t = Test::Mojo->new('Celestial');
|
---|
29 |
|
---|
30 | # Post a JSON document
|
---|
31 | $t->post_ok('/notifications' => json => {event => 'full moon'})
|
---|
32 | ->status_is(201)
|
---|
33 | ->json_is('/message' => 'notification created');
|
---|
34 |
|
---|
35 | # Perform GET requests and look at the responses
|
---|
36 | $t->get_ok('/sunrise')
|
---|
37 | ->status_is(200)
|
---|
38 | ->content_like(qr/ am$/);
|
---|
39 | $t->get_ok('/sunset')
|
---|
40 | ->status_is(200)
|
---|
41 | ->content_like(qr/ pm$/);
|
---|
42 |
|
---|
43 | # Post a URL-encoded form
|
---|
44 | $t->post_ok('/insurance' => form => {name => 'Jimmy', amount => 'â¬3.000.000'})
|
---|
45 | ->status_is(200);
|
---|
46 |
|
---|
47 | # Use Test::More's like() to check the response
|
---|
48 | like $t->tx->res->dom->at('div#thanks')->text, qr/thank you/, 'thanks';
|
---|
49 |
|
---|
50 | done_testing();
|
---|
51 |
|
---|
52 | In the rest of this document we'll explore these concepts and others related to
|
---|
53 | L<Test::Mojo>.
|
---|
54 |
|
---|
55 | =head1 CONCEPTS
|
---|
56 |
|
---|
57 | Essentials every L<Mojolicious> developer should know.
|
---|
58 |
|
---|
59 | =head2 L<Test::Mojo> at a glance
|
---|
60 |
|
---|
61 | The L<Test::More> module bundled with Perl includes several primitive test
|
---|
62 | assertions, such as C<ok>, C<is>, C<isnt>, C<like>, C<unlike>, C<cmp_ok>, etc.
|
---|
63 | An assertion "passes" if its expression returns a true value. The assertion
|
---|
64 | method prints "ok" or "not ok" if an assertion passes or fails (respectively).
|
---|
65 |
|
---|
66 | L<Test::Mojo> supplies additional test assertions organized around the web
|
---|
67 | application request/response transaction (transport, response headers, response
|
---|
68 | bodies, etc.), and WebSocket communications.
|
---|
69 |
|
---|
70 | One interesting thing of note: the return value of L<Test::Mojo> object
|
---|
71 | assertions is always the test object itself, allowing us to "chain" test
|
---|
72 | assertion methods. So rather than grouping related test statements like this:
|
---|
73 |
|
---|
74 | $t->get_ok('/frogs');
|
---|
75 | $t->status_is(200);
|
---|
76 | $t->content_like(qr/bullfrog/);
|
---|
77 | $t->content_like(qr/hypnotoad/);
|
---|
78 |
|
---|
79 | Method chaining allows us to connect test assertions that belong together:
|
---|
80 |
|
---|
81 | $t->get_ok('/frogs')
|
---|
82 | ->status_is(200)
|
---|
83 | ->content_like(qr/bullfrog/)
|
---|
84 | ->content_like(qr/hypnotoad/);
|
---|
85 |
|
---|
86 | This makes for a much more I<concise> and I<coherent> testing experience:
|
---|
87 | concise because we are not repeating the invocant for each test, and coherent
|
---|
88 | because assertions that belong to the same request are syntactically bound in
|
---|
89 | the same method chain.
|
---|
90 |
|
---|
91 | Occasionally it makes sense to break up a test to perform more complex
|
---|
92 | assertions on a response. L<Test::Mojo> exposes the entire transaction object so
|
---|
93 | you can get all the data you need from a response:
|
---|
94 |
|
---|
95 | $t->put_ok('/bees' => json => {type => 'worker', name => 'Karl'})
|
---|
96 | ->status_is(202)
|
---|
97 | ->json_has('/id');
|
---|
98 |
|
---|
99 | # Pull out the id from the response
|
---|
100 | my $newbee = $t->tx->res->json('/id');
|
---|
101 |
|
---|
102 | # Make a new request with data from the previous response
|
---|
103 | $t->get_ok("/bees/$newbee")
|
---|
104 | ->status_is(200)
|
---|
105 | ->json_is('/name' => 'Karl');
|
---|
106 |
|
---|
107 | The L<Test::Mojo> object is I<stateful>. As long as we haven't started a new
|
---|
108 | transaction by invoking one of the C<*_ok> methods, the request and response
|
---|
109 | objects from the previous transaction are available in the L<Test::Mojo>
|
---|
110 | object:
|
---|
111 |
|
---|
112 | # First transaction
|
---|
113 | $t->get_ok('/frogs?q=bullfrog' => {'Content-Type' => 'application/json'})
|
---|
114 | ->status_is(200)
|
---|
115 | ->json_like('/0/species' => qr/catesbeianus/i);
|
---|
116 |
|
---|
117 | # Still first transaction
|
---|
118 | $t->content_type_is('application/json');
|
---|
119 |
|
---|
120 | # Second transaction
|
---|
121 | $t->get_ok('/frogs?q=banjo' => {'Content-Type' => 'text/html'})
|
---|
122 | ->status_is(200)
|
---|
123 | ->content_like(qr/interioris/i);
|
---|
124 |
|
---|
125 | # Still second transaction
|
---|
126 | $t->content_type_is('text/html');
|
---|
127 |
|
---|
128 | This statefulness also enables L<Test::Mojo> to handle sessions, follow
|
---|
129 | redirects, and inspect past responses during a redirect.
|
---|
130 |
|
---|
131 | =head2 The L<Test::Mojo> object
|
---|
132 |
|
---|
133 | The L<Test::Mojo> object manages the Mojolicious application lifecycle (if a
|
---|
134 | Mojolicious application class is supplied) as well as exposes the built-in
|
---|
135 | L<Mojo::UserAgent> object. To create a bare L<Test::Mojo> object:
|
---|
136 |
|
---|
137 | my $t = Test::Mojo->new;
|
---|
138 |
|
---|
139 | This object initializes a L<Mojo::UserAgent> object and provides a variety of
|
---|
140 | test assertion methods for accessing a web application. For example, with this
|
---|
141 | object, we could test any running web application:
|
---|
142 |
|
---|
143 | $t->get_ok('https://www.google.com/')
|
---|
144 | ->status_is(200)
|
---|
145 | ->content_like(qr/search/i);
|
---|
146 |
|
---|
147 | You can access the user agent directly if you want to make web requests without
|
---|
148 | triggering test assertions:
|
---|
149 |
|
---|
150 | my $tx = $t->ua->post(
|
---|
151 | 'https://duckduckgo.com/html' => form => {q => 'hypnotoad'});
|
---|
152 | $tx->result->dom->find('a.result__a')->each(sub { say $_->text });
|
---|
153 |
|
---|
154 | See L<Mojo::UserAgent> for the complete API and return values.
|
---|
155 |
|
---|
156 | =head2 Testing Mojolicious applications
|
---|
157 |
|
---|
158 | If you pass the name of a L<Mojolicious> application class (e.g., 'MyApp') to
|
---|
159 | the L<Test::Mojo> constructor, L<Test::Mojo> will instantiate the class and
|
---|
160 | start it, and cause it to listen on a random (unused) port number. Testing a
|
---|
161 | Mojolicious application using L<Test::Mojo> will never conflict with running
|
---|
162 | applications, including the application you're testing.
|
---|
163 |
|
---|
164 | The L<Mojo::UserAgent> object in L<Test::Mojo> will know where the application
|
---|
165 | is running and make requests to it. Once the tests have completed, the
|
---|
166 | L<Mojolicious> application will be torn down.
|
---|
167 |
|
---|
168 | # Listens on localhost:32114 (some unused TCP port)
|
---|
169 | my $t = Test::Mojo->new('Frogs');
|
---|
170 |
|
---|
171 | This object initializes a L<Mojo::UserAgent> object, loads the Mojolicious
|
---|
172 | application C<Frogs>, binds and listens on a free TCP port (e.g., 32114), and
|
---|
173 | starts the application event loop. When the L<Test::Mojo> object (C<$t>) goes
|
---|
174 | out of scope, the application is stopped.
|
---|
175 |
|
---|
176 | Relative URLs in the test object method assertions (C<get_ok>, C<post_ok>, etc.)
|
---|
177 | will be sent to the Mojolicious application started by L<Test::Mojo>:
|
---|
178 |
|
---|
179 | # Rewritten to "http://localhost:32114/frogs"
|
---|
180 | $t->get_ok('/frogs');
|
---|
181 |
|
---|
182 | L<Test::Mojo> has a lot of handy shortcuts built into it to make testing
|
---|
183 | L<Mojolicious> or L<Mojolicious::Lite> applications enjoyable.
|
---|
184 |
|
---|
185 | =head3 An example
|
---|
186 |
|
---|
187 | Let's spin up a Mojolicious application using C<mojo generate app MyApp>. The
|
---|
188 | C<mojo> utility will create a working application and a C<t> directory with a
|
---|
189 | working test file:
|
---|
190 |
|
---|
191 | $ mojo generate app MyApp
|
---|
192 | [mkdir] /my_app/script
|
---|
193 | [write] /my_app/script/my_app
|
---|
194 | [chmod] /my_app/script/my_app 744
|
---|
195 | ...
|
---|
196 | [mkdir] /my_app/t
|
---|
197 | [write] /my_app/t/basic.t
|
---|
198 | ...
|
---|
199 |
|
---|
200 | Let's run the tests (we'll create the C<log> directory to quiet the application
|
---|
201 | output):
|
---|
202 |
|
---|
203 | $ cd my_app
|
---|
204 | $ mkdir log
|
---|
205 | $ prove -lv t
|
---|
206 | t/basic.t ..
|
---|
207 | ok 1 - GET /
|
---|
208 | ok 2 - 200 OK
|
---|
209 | ok 3 - content is similar
|
---|
210 | 1..3
|
---|
211 | ok
|
---|
212 | All tests successful.
|
---|
213 | Files=1, Tests=3, 0 wallclock secs ( 0.03 usr 0.01 sys + 0.33 cusr 0.07
|
---|
214 | csys = 0.44 CPU)
|
---|
215 | Result: PASS
|
---|
216 |
|
---|
217 | The boilerplate test file looks like this:
|
---|
218 |
|
---|
219 | use Mojo::Base -strict;
|
---|
220 |
|
---|
221 | use Test::More;
|
---|
222 | use Test::Mojo;
|
---|
223 |
|
---|
224 | my $t = Test::Mojo->new('MyApp');
|
---|
225 | $t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i);
|
---|
226 |
|
---|
227 | done_testing();
|
---|
228 |
|
---|
229 | Here we can see our application class name C<MyApp> is passed to the
|
---|
230 | L<Test::Mojo> constructor. Under the hood, L<Test::Mojo> creates a new
|
---|
231 | L<Mojo::Server> instance, loads C<MyApp> (which we just created), and runs the
|
---|
232 | application. We write our tests with relative URLs because L<Test::Mojo> takes
|
---|
233 | care of getting the request to the running test application (since its port may
|
---|
234 | change between runs).
|
---|
235 |
|
---|
236 | =head3 Testing with configuration data
|
---|
237 |
|
---|
238 | We can alter the behavior of our application using environment variables (such
|
---|
239 | as C<MOJO_MODE>) and through configuration values. One nice feature of
|
---|
240 | L<Test::Mojo> is its ability to pass configuration values directly from its
|
---|
241 | constructor.
|
---|
242 |
|
---|
243 | Let's modify our application and add a "feature flag" to enable a new feature
|
---|
244 | when the C<enable_weather> configuration value is set:
|
---|
245 |
|
---|
246 | # Load configuration from hash returned by "my_app.conf"
|
---|
247 | my $config = $self->plugin('Config');
|
---|
248 |
|
---|
249 | # Normal route to controller
|
---|
250 | $r->get('/')->to('example#welcome');
|
---|
251 |
|
---|
252 | # NEW: this route only exists if "enable_weather" is set in the configuration
|
---|
253 | if ($config->{enable_weather}) {
|
---|
254 | $r->get('/weather' => sub { shift->render(text => "It's hot! ð¥") }
|
---|
255 | }
|
---|
256 |
|
---|
257 | To test this new feature, we don't even need to create a configuration fileâwe
|
---|
258 | can simply pass the configuration data to the application directly via
|
---|
259 | L<Test::Mojo>'s constructor:
|
---|
260 |
|
---|
261 | my $t = Test::Mojo->new(MyApp => {enable_weather => 1});
|
---|
262 | $t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i);
|
---|
263 | $t->get_ok('/weather')->status_is(200)->content_like(qr/ð¥/);
|
---|
264 |
|
---|
265 | When we run these tests, L<Test::Mojo> will pass this configuration data to the
|
---|
266 | application, which will cause it to create a special C</weather> route that we
|
---|
267 | can access in our tests. Unless C<enable_weather> is set in a configuration
|
---|
268 | file, this route will not exist when the application runs. Feature flags like
|
---|
269 | this allow us to do soft rollouts of features, targeting a small audience for a
|
---|
270 | period of time. Once the feature has been proven, we can refactor the
|
---|
271 | conditional and make it a full release.
|
---|
272 |
|
---|
273 | This example shows how easy it is to start testing a Mojolicious application and
|
---|
274 | how to set specific application configuration directives from a test file.
|
---|
275 |
|
---|
276 | =head3 Testing application helpers
|
---|
277 |
|
---|
278 | Let's say we register a helper in our application to generate an HTTP Basic
|
---|
279 | Authorization header:
|
---|
280 |
|
---|
281 | use Mojo::Util 'b64_encode';
|
---|
282 |
|
---|
283 | app->helper(basic_auth => sub {
|
---|
284 | my ($c, @values) = @_;
|
---|
285 | return {Authorization => 'Basic ' . b64_encode join(':' => @values), ''};
|
---|
286 | });
|
---|
287 |
|
---|
288 | How do we test application helpers like this? L<Test::Mojo> has access to the
|
---|
289 | application object, which allows us to invoke helpers from our test file:
|
---|
290 |
|
---|
291 | my $t = Test::Mojo->new('MyApp');
|
---|
292 |
|
---|
293 | is_deeply $t->app->basic_auth(bif => "Bif's Passwerdd"),
|
---|
294 | {Authorization => 'Basic YmlmOkJpZidzIFBhc3N3ZXJkZA=='},
|
---|
295 | 'correct header value';
|
---|
296 |
|
---|
297 | Any aspect of the application (helpers, plugins, routes, etc.) can be
|
---|
298 | introspected from L<Test::Mojo> through the application object. This enables us
|
---|
299 | to get deep test coverage of L<Mojolicious>-based applications.
|
---|
300 |
|
---|
301 | =head1 ASSERTIONS
|
---|
302 |
|
---|
303 | This section describes the basic test assertions supplied by L<Test::Mojo>.
|
---|
304 | There are four broad categories of assertions for HTTP requests:
|
---|
305 |
|
---|
306 | =over 2
|
---|
307 |
|
---|
308 | =item * HTTP requests
|
---|
309 |
|
---|
310 | =item * HTTP response status
|
---|
311 |
|
---|
312 | =item * HTTP response headers
|
---|
313 |
|
---|
314 | =item * HTTP response content/body
|
---|
315 |
|
---|
316 | =back
|
---|
317 |
|
---|
318 | WebSocket test assertions are covered in L</Testing WebSocket web services>.
|
---|
319 |
|
---|
320 | =head2 HTTP request assertions
|
---|
321 |
|
---|
322 | L<Test::Mojo> has a L<Mojo::UserAgent> object that allows it to make HTTP
|
---|
323 | requests and check for HTTP transport errors. HTTP request assertions include
|
---|
324 | C<get_ok>, C<post_ok>, etc. These assertions do not test whether the request
|
---|
325 | was handled I<successfully>, only that the web application handled the request
|
---|
326 | in an HTTP compliant way.
|
---|
327 |
|
---|
328 | You may also make HTTP requests using custom verbs (beyond C<GET>, C<POST>,
|
---|
329 | C<PUT>, etc.) by building your own transaction object. See
|
---|
330 | L</"Custom transactions"> below.
|
---|
331 |
|
---|
332 | =head3 Using HTTP request assertions
|
---|
333 |
|
---|
334 | To post a URL-encoded form to the C</calls> endpoint of an application, we
|
---|
335 | simply use the C<form> content type shortcut:
|
---|
336 |
|
---|
337 | $t->post_ok('/calls' => form => {to => '+43.55.555.5555'});
|
---|
338 |
|
---|
339 | Which will create the following HTTP request:
|
---|
340 |
|
---|
341 | POST /calls HTTP/1.1
|
---|
342 | Content-Length: 20
|
---|
343 | Content-Type: application/x-www-form-urlencoded
|
---|
344 |
|
---|
345 | to=%2B43.55.555.5555
|
---|
346 |
|
---|
347 | The C<*_ok> HTTP request assertion methods accept the same arguments as their
|
---|
348 | corresponding L<Mojo::UserAgent> methods (except for the callback argument).
|
---|
349 | This allows us to set headers and build query strings for authentic test
|
---|
350 | situations:
|
---|
351 |
|
---|
352 | $t->get_ok('/internal/personnel' => {Authorization => 'Token secret-password'}
|
---|
353 | => form => {q => 'Professor Plum'});
|
---|
354 |
|
---|
355 | which generates the following request:
|
---|
356 |
|
---|
357 | GET /internal/personnel?q=Professor+Plum HTTP/1.1
|
---|
358 | Content-Length: 0
|
---|
359 | Authorization: Token secret-password
|
---|
360 |
|
---|
361 | The C<form> content generator (see L<Mojo::UserAgent::Transactor>) will generate
|
---|
362 | a query string for C<GET> requests and C<application/x-www-form-urlencoded> or
|
---|
363 | C<multipart/form-data> for POST requests.
|
---|
364 |
|
---|
365 | While these C<*_ok> assertions make the HTTP I<requests> we expect, they tell us
|
---|
366 | little about I<how well> the application handled the request. The application
|
---|
367 | we're testing might have returned any content-type, body, or HTTP status code
|
---|
368 | (200, 302, 400, 404, 500, etc.) and we wouldn't know it.
|
---|
369 |
|
---|
370 | L<Test::Mojo> provides assertions to test almost every aspect of the HTTP
|
---|
371 | response, including the HTTP response status code, the value of the
|
---|
372 | C<Content-Type> header, and other arbitrary HTTP header information.
|
---|
373 |
|
---|
374 | =head2 HTTP response status code
|
---|
375 |
|
---|
376 | While not technically an HTTP header, the status line is the first line in an
|
---|
377 | HTTP response and is followed by the response headers. Testing the response
|
---|
378 | status code is common in REST-based and other web applications that use the HTTP
|
---|
379 | status codes to broadly indicate the type of response the server is returning.
|
---|
380 |
|
---|
381 | Testing the status code is as simple as adding the C<status_is> assertion:
|
---|
382 |
|
---|
383 | $t->post_ok('/doorbell' => form => {action => 'ring once'})
|
---|
384 | ->status_is(200);
|
---|
385 |
|
---|
386 | Along with C<status_isnt>, this will cover most needs. For more elaborate status
|
---|
387 | code testing, you can access the response internals directly:
|
---|
388 |
|
---|
389 | $t->post_ok('/doorbell' => form => {action => 'ring once'});
|
---|
390 | is $t->tx->res->message, 'Moved Permanently', 'try next door';
|
---|
391 |
|
---|
392 | =head2 HTTP response headers
|
---|
393 |
|
---|
394 | L<Test::Mojo> allows us to inspect and make assertions about HTTP response
|
---|
395 | headers. The C<Content-Type> header is commonly tested and has its own
|
---|
396 | assertion:
|
---|
397 |
|
---|
398 | $t->get_ok('/map-of-the-world.pdf')
|
---|
399 | ->content_type_is('application/pdf');
|
---|
400 |
|
---|
401 | This is equivalent to the more verbose:
|
---|
402 |
|
---|
403 | $t->get_ok('/map-of-the-world.pdf')
|
---|
404 | ->header_is('Content-Type' => 'application/pdf');
|
---|
405 |
|
---|
406 | We can test for multiple headers in a single response using method chains:
|
---|
407 |
|
---|
408 | $t->get_ok('/map-of-the-world.pdf')
|
---|
409 | ->content_type_is('application/pdf')
|
---|
410 | ->header_isnt('Compression' => 'gzip')
|
---|
411 | ->header_unlike('Server' => qr/IIS/i);
|
---|
412 |
|
---|
413 | =head2 HTTP response content assertions
|
---|
414 |
|
---|
415 | L<Test::Mojo> also exposes a rich set of assertions for testing the body of a
|
---|
416 | response, whether that body be HTML, plain-text, or JSON. The C<content_*>
|
---|
417 | methods look at the body of the response as plain text (as defined by the
|
---|
418 | response's character set):
|
---|
419 |
|
---|
420 | $t->get_ok('/scary-things/spiders.json')
|
---|
421 | ->content_is('{"arachnid":"brown recluse"}');
|
---|
422 |
|
---|
423 | Although this is a JSON document, C<content_is> treats it as if it were a text
|
---|
424 | document. This may be useful for situations where we're looking for a particular
|
---|
425 | string and not concerned with the structure of the document. For example, we can
|
---|
426 | do the same thing with an HTML document:
|
---|
427 |
|
---|
428 | $t->get_ok('/scary-things/spiders.html')
|
---|
429 | ->content_like(qr{<title>All The Spiders</title>});
|
---|
430 |
|
---|
431 | But because L<Test::Mojo> has access to everything that L<Mojo::UserAgent> does,
|
---|
432 | we can introspect JSON documents as well as DOM-based documents (HTML, XML) with
|
---|
433 | assertions that allow us to check for the existence of elements as well as
|
---|
434 | inspect the content of text nodes.
|
---|
435 |
|
---|
436 | =head3 JSON response assertions
|
---|
437 |
|
---|
438 | L<Test::Mojo>'s L<Mojo::UserAgent> has access to a JSON parser, which allows us
|
---|
439 | to test to see if a JSON response contains a value at a location in the document
|
---|
440 | using JSON pointer syntax:
|
---|
441 |
|
---|
442 | $t->get_ok('/animals/friendly.json')
|
---|
443 | ->json_has('/beings/jeremiah/age');
|
---|
444 |
|
---|
445 | This assertion tells us that the C<friendly.json> document contains a value at
|
---|
446 | the C</beings/jeremiah/age> JSON pointer location. We can also inspect the value
|
---|
447 | at JSON pointer locations:
|
---|
448 |
|
---|
449 | $t->get_ok('/animals/friendly.json')
|
---|
450 | ->json_has('/beings/jeremiah/age')
|
---|
451 | ->json_is('/beings/jeremiah/age' => 42)
|
---|
452 | ->json_like('/beings/jeremiah/species' => qr/bullfrog/i);
|
---|
453 |
|
---|
454 | JSON pointer syntax makes testing JSON responses simple and readable.
|
---|
455 |
|
---|
456 | =head3 DOM response assertions
|
---|
457 |
|
---|
458 | We can also inspect HTML and XML responses using the L<Mojo::DOM> parser in the
|
---|
459 | user agent. Here are a few examples from the L<Test::Mojo> documentation:
|
---|
460 |
|
---|
461 | $t->text_is('div.foo[x=y]' => 'Hello!');
|
---|
462 | $t->text_is('html head title' => 'Hello!', 'right title');
|
---|
463 |
|
---|
464 | The L<Mojo::DOM> parser uses the CSS selector syntax described in
|
---|
465 | L<Mojo::DOM::CSS>, allowing us to test for values in HTML and XML documents
|
---|
466 | without resorting to typically verbose and inflexible DOM traversal methods.
|
---|
467 |
|
---|
468 | =head1 ADVANCED TOPICS
|
---|
469 |
|
---|
470 | This section describes some complex (but common) testing situations that
|
---|
471 | L<Test::Mojo> excels in making simple.
|
---|
472 |
|
---|
473 | =head2 Redirects
|
---|
474 |
|
---|
475 | The L<Mojo::UserAgent> object in L<Test::Mojo> can handle HTTP redirections
|
---|
476 | internally to whatever level you need. Let's say we have a web service that
|
---|
477 | redirects C</1> to C</2>, C</2> redirects to C</3>, C</3> redirects to C</4>,
|
---|
478 | and C</4> redirects to C</5>:
|
---|
479 |
|
---|
480 | GET /1
|
---|
481 |
|
---|
482 | returns:
|
---|
483 |
|
---|
484 | 302 Found
|
---|
485 | Location: /2
|
---|
486 |
|
---|
487 | and:
|
---|
488 |
|
---|
489 | GET /2
|
---|
490 |
|
---|
491 | returns:
|
---|
492 |
|
---|
493 | 302 Found
|
---|
494 | Location: /3
|
---|
495 |
|
---|
496 | and so forth, up to C</5>:
|
---|
497 |
|
---|
498 | GET /5
|
---|
499 |
|
---|
500 | which returns the data we wanted:
|
---|
501 |
|
---|
502 | 200 OK
|
---|
503 |
|
---|
504 | {"message":"this is five"}
|
---|
505 |
|
---|
506 | We can tell the user agent in L<Test::Mojo> how to deal with redirects. Each
|
---|
507 | test is making a request to C<GET /1>, but we vary the number of redirects the
|
---|
508 | user agent should follow with each test:
|
---|
509 |
|
---|
510 | my $t = Test::Mojo->new;
|
---|
511 |
|
---|
512 | $t->get_ok('/1')
|
---|
513 | ->header_is(location => '/2');
|
---|
514 |
|
---|
515 | $t->ua->max_redirects(1);
|
---|
516 | $t->get_ok('/1')
|
---|
517 | ->header_is(location => '/3');
|
---|
518 |
|
---|
519 | $t->ua->max_redirects(2);
|
---|
520 | $t->get_ok('/1')
|
---|
521 | ->header_is(location => '/4');
|
---|
522 |
|
---|
523 | # Look at the previous hop
|
---|
524 | is $t->tx->previous->res->headers->location, '/3', 'previous redirect';
|
---|
525 |
|
---|
526 | $t->ua->max_redirects(3);
|
---|
527 | $t->get_ok('/1')
|
---|
528 | ->header_is(location => '/5');
|
---|
529 |
|
---|
530 | $t->ua->max_redirects(4);
|
---|
531 | $t->get_ok('/1')
|
---|
532 | ->json_is('/message' => 'this is five');
|
---|
533 |
|
---|
534 | When we set C<max_redirects>, it stays set for the life of the test object until
|
---|
535 | we change it.
|
---|
536 |
|
---|
537 | L<Test::Mojo>'s handling of HTTP redirects eliminates the need for making many,
|
---|
538 | sometimes an unknown number, of redirections to keep testing precise and easy to
|
---|
539 | follow (ahem).
|
---|
540 |
|
---|
541 | =head2 Cookies and session management
|
---|
542 |
|
---|
543 | We can use L<Test::Mojo> to test applications that keep session state in
|
---|
544 | cookies. By default, the L<Mojo::UserAgent> object in L<Test::Mojo> will manage
|
---|
545 | session for us by saving and sending cookies automatically, just like common web
|
---|
546 | browsers:
|
---|
547 |
|
---|
548 | use Mojo::Base -strict;
|
---|
549 |
|
---|
550 | use Test::More;
|
---|
551 | use Test::Mojo;
|
---|
552 |
|
---|
553 | my $t = Test::Mojo->new('MyApp');
|
---|
554 |
|
---|
555 | # No authorization cookie
|
---|
556 | $t->get_ok('/')
|
---|
557 | ->status_is(401)
|
---|
558 | ->content_is('Please log in');
|
---|
559 |
|
---|
560 | # Application sets an authorization cookie
|
---|
561 | $t->post_ok('/login' => form => {password => 'let me in'})
|
---|
562 | ->status_is(200)
|
---|
563 | ->content_is('You are logged in');
|
---|
564 |
|
---|
565 | # Sends the cookie from the previous transaction
|
---|
566 | $t->get_ok('/')
|
---|
567 | ->status_is(200)
|
---|
568 | ->content_like(qr/You logged in at \d+/);
|
---|
569 |
|
---|
570 | # Clear the cookies
|
---|
571 | $t->reset_session;
|
---|
572 |
|
---|
573 | # No authorization cookie again
|
---|
574 | $t->get_ok('/')
|
---|
575 | ->status_is(401)
|
---|
576 | ->content_is('Please log in');
|
---|
577 |
|
---|
578 | We can also inspect cookies in responses for special values through the
|
---|
579 | transaction's response (L<Mojo::Message::Response>) object:
|
---|
580 |
|
---|
581 | $t->get_ok('/');
|
---|
582 | like $t->tx->res->cookie('smarty'), qr/smarty=pants/, 'cookie found';
|
---|
583 |
|
---|
584 | =head2 Custom transactions
|
---|
585 |
|
---|
586 | Let's say we have an application that responds to a new HTTP verb C<RING> and to
|
---|
587 | use it we must also pass in a secret cookie value. This is not a problem. We can
|
---|
588 | test the application by creating a L<Mojo::Transaction> object, setting the
|
---|
589 | cookie (see L<Mojo::Message::Request>), then passing the transaction object to
|
---|
590 | C<request_ok>:
|
---|
591 |
|
---|
592 | # Use custom "RING" verb
|
---|
593 | my $tx = $t->ua->build_tx(RING => '/doorbell');
|
---|
594 |
|
---|
595 | # Set a special cookie
|
---|
596 | $tx->req->cookies({name => 'Secret', value => "don't tell anybody"});
|
---|
597 |
|
---|
598 | # Make the request
|
---|
599 | $t->request_ok($tx)
|
---|
600 | ->status_is(200)
|
---|
601 | ->json_is('/status' => 'ding dong');
|
---|
602 |
|
---|
603 | =head2 Testing WebSocket web services
|
---|
604 |
|
---|
605 | While the message flow on WebSocket connections can be rather dynamic, it more
|
---|
606 | often than not is quite predictable, which allows this rather pleasant
|
---|
607 | L<Test::Mojo> WebSocket API to be used:
|
---|
608 |
|
---|
609 | use Mojo::Base -strict;
|
---|
610 |
|
---|
611 | use Test::More;
|
---|
612 | use Test::Mojo;
|
---|
613 |
|
---|
614 | # Test echo web service
|
---|
615 | my $t = Test::Mojo->new('EchoService');
|
---|
616 | $t->websocket_ok('/echo')
|
---|
617 | ->send_ok('Hello Mojo!')
|
---|
618 | ->message_ok
|
---|
619 | ->message_is('echo: Hello Mojo!')
|
---|
620 | ->finish_ok;
|
---|
621 |
|
---|
622 | # Test JSON web service
|
---|
623 | $t->websocket_ok('/echo.json')
|
---|
624 | ->send_ok({json => {test => [1, 2, 3]}})
|
---|
625 | ->message_ok
|
---|
626 | ->json_message_is('/test' => [1, 2, 3])
|
---|
627 | ->finish_ok;
|
---|
628 |
|
---|
629 | done_testing();
|
---|
630 |
|
---|
631 | Because of their inherent asynchronous nature, testing WebSocket communications
|
---|
632 | can be tricky. The L<Test::Mojo> WebSocket assertions serialize messages via
|
---|
633 | event loop primitives. This enables us to treat WebSocket messages as if they
|
---|
634 | were using the same request-response communication pattern we're accustomed to
|
---|
635 | with HTTP.
|
---|
636 |
|
---|
637 | To illustrate, let's walk through these tests. In the first test, we use the
|
---|
638 | C<websocket_ok> assertion to ensure that we can connect to our application's
|
---|
639 | WebSocket route at C</echo> and that it's "speaking" WebSocket protocol to us.
|
---|
640 | The next C<send_ok> assertion tests the connection again (in case it closed, for
|
---|
641 | example) and attempts to send the message C<Hello Mojo!>. The next assertion,
|
---|
642 | C<message_ok>, blocks (using the L<Mojo::IOLoop> singleton in the application)
|
---|
643 | and waits for a response from the server. The response is then compared with
|
---|
644 | C<'echo: Hello Mojo!'> in the C<message_is> assertion, and finally we close and
|
---|
645 | test our connection status again with C<finish_ok>.
|
---|
646 |
|
---|
647 | The second test is like the first, but now we're sending and expecting JSON
|
---|
648 | documents at C</echo.json>. In the C<send_ok> assertion we take advantage of
|
---|
649 | L<Mojo::UserAgent>'s JSON content generator (see L<Mojo::UserAgent::Transactor>)
|
---|
650 | to marshal hash and array references into JSON documents, and then send them as
|
---|
651 | a WebSocket message. We wait (block) for a response from the server with
|
---|
652 | C<message_ok>. Then because we're expecting a JSON document back, we can
|
---|
653 | leverage C<json_message_ok> which parses the WebSocket response body and returns
|
---|
654 | an object we can access through L<Mojo::JSON::Pointer> syntax. Then we close
|
---|
655 | (and test) our WebSocket connection.
|
---|
656 |
|
---|
657 | Testing WebSocket servers does not get any simpler than with L<Test::Mojo>.
|
---|
658 |
|
---|
659 | =head2 Extending L<Test::Mojo>
|
---|
660 |
|
---|
661 | If you see that you're writing a lot of test assertions that aren't chainable,
|
---|
662 | you may benefit from writing your own test assertions. Let's say we want to test
|
---|
663 | the C<Location> header after a redirect. We'll create a new class with
|
---|
664 | L<Role::Tiny> that implements a test assertion named C<location_is>:
|
---|
665 |
|
---|
666 | package Test::Mojo::Role::Location;
|
---|
667 | use Mojo::Base -role;
|
---|
668 |
|
---|
669 | use Test::More;
|
---|
670 |
|
---|
671 | sub location_is {
|
---|
672 | my ($t, $value, $desc) = @_;
|
---|
673 | $desc ||= "Location: $value";
|
---|
674 | local $Test::Builder::Level = $Test::Builder::Level + 1;
|
---|
675 | return $t->success(is($t->tx->res->headers->location, $value, $desc));
|
---|
676 | }
|
---|
677 |
|
---|
678 | 1;
|
---|
679 |
|
---|
680 | When we make new test assertions using roles, we want to use method signatures
|
---|
681 | that match other C<*_is> methods in L<Test::Mojo>, so here we accept the test
|
---|
682 | object, the value to compare, and an optional description.
|
---|
683 |
|
---|
684 | We assign a default description value (C<$desc>), set the L<Test::Builder>
|
---|
685 | C<Level> global variable one level higher (which tells L<Test::Builder> how far
|
---|
686 | up the call stack to look when something fails), then we use L<Test::More>'s
|
---|
687 | C<is> function to compare the location header with the expected header value. We
|
---|
688 | wrap that in the C<success> attribute, which records the boolean test result and
|
---|
689 | propagates the L<Test::Mojo> object for method chaining.
|
---|
690 |
|
---|
691 | With this new package, we're ready to compose a new test object that uses the
|
---|
692 | role:
|
---|
693 |
|
---|
694 | my $t = Test::Mojo->with_roles('+Location')->new('MyApp');
|
---|
695 |
|
---|
696 | $t->post_ok('/redirect/mojo' => json => {message => 'Mojo, here I come!'})
|
---|
697 | ->status_is(302)
|
---|
698 | ->location_is('http://mojolicious.org')
|
---|
699 | ->or(sub { diag 'I miss tempire.' });
|
---|
700 |
|
---|
701 | In this section we've covered how to add custom test assertions to L<Test::Mojo>
|
---|
702 | with roles and how to use those roles to simplify testing.
|
---|
703 |
|
---|
704 | =head1 MORE
|
---|
705 |
|
---|
706 | You can continue with L<Mojolicious::Guides> now or take a look at the
|
---|
707 | L<Mojolicious wiki|http://github.com/kraih/mojo/wiki>, which contains a lot more
|
---|
708 | documentation and examples by many different authors.
|
---|
709 |
|
---|
710 | =head1 SUPPORT
|
---|
711 |
|
---|
712 | If you have any questions the documentation might not yet answer, don't
|
---|
713 | hesitate to ask on the
|
---|
714 | L<mailing list|http://groups.google.com/group/mojolicious> or the official IRC
|
---|
715 | channel C<#mojo> on C<irc.perl.org>
|
---|
716 | (L<chat now!|https://chat.mibbit.com/?channel=%23mojo&server=irc.perl.org>).
|
---|
717 |
|
---|
718 | =cut
|
---|