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