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('https://duckduckgo.com/html' => form => {q => 'hypnotoad'});
152 $tx->result->dom->find('a.result__a')->each(sub { say $_->text });
153
154 See Mojo::UserAgent for the complete API and return values.
155
156 Testing Mojolicious applications
157 If you pass the name of a Mojolicious application class (e.g., 'MyApp')
158 to the Test::Mojo constructor, Test::Mojo will instantiate the class
159 and start it, and cause it to listen on a random (unused) port number.
160 Testing a Mojolicious application using Test::Mojo will never conflict
161 with running applications, including the application you're testing.
162
163 The Mojo::UserAgent object in Test::Mojo will know where the
164 application is running and make requests to it. Once the tests have
165 completed, the Mojolicious application will be torn down.
166
167 # Listens on localhost:32114 (some unused TCP port)
168 my $t = Test::Mojo->new('Frogs');
169
170 This object initializes a Mojo::UserAgent object, loads the Mojolicious
171 application "Frogs", binds and listens on a free TCP port (e.g.,
172 32114), and starts the application event loop. When the Test::Mojo
173 object ($t) goes out of scope, the application is stopped.
174
175 Relative URLs in the test object method assertions ("get_ok",
176 "post_ok", etc.) will be sent to the Mojolicious application started by
177 Test::Mojo:
178
179 # Rewritten to "http://localhost:32114/frogs"
180 $t->get_ok('/frogs');
181
182 Test::Mojo has a lot of handy shortcuts built into it to make testing
183 Mojolicious or Mojolicious::Lite applications enjoyable.
184
185 An example
186
187 Let's spin up a Mojolicious application using "mojo generate app
188 MyApp". The "mojo" utility will create a working application and a "t"
189 directory with a 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 "log" directory to quiet the
201 application 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 csys = 0.44 CPU)
214 Result: PASS
215
216 The boilerplate test file looks like this:
217
218 use Mojo::Base -strict;
219
220 use Test::More;
221 use Test::Mojo;
222
223 my $t = Test::Mojo->new('MyApp');
224 $t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i);
225
226 done_testing();
227
228 Here we can see our application class name "MyApp" is passed to the
229 Test::Mojo constructor. Under the hood, Test::Mojo creates a new
230 Mojo::Server instance, loads "MyApp" (which we just created), and runs
231 the application. We write our tests with relative URLs because
232 Test::Mojo takes care of getting the request to the running test
233 application (since its port may change between runs).
234
235 Testing with configuration data
236
237 We can alter the behavior of our application using environment
238 variables (such as "MOJO_MODE") and through configuration values. One
239 nice feature of Test::Mojo is its ability to pass configuration values
240 directly from its constructor.
241
242 Let's modify our application and add a "feature flag" to enable a new
243 feature when the "enable_weather" configuration value is set:
244
245 # Load configuration from hash returned by "my_app.conf"
246 my $config = $self->plugin('Config');
247
248 # Normal route to controller
249 $r->get('/')->to('example#welcome');
250
251 # NEW: this route only exists if "enable_weather" is set in the configuration
252 if ($config->{enable_weather}) {
253 $r->get('/weather' => sub ($c) {
254 $c->render(text => "It's hot! 🔥");
255 });
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 ($c, @values) {
287 return {Authorization => 'Basic ' . b64_encode join(':' => @values), ''};
288 });
289
290 How do we test application helpers like this? Test::Mojo has access to
291 the application object, which allows us to invoke helpers from our test
292 file:
293
294 my $t = Test::Mojo->new('MyApp');
295
296 is_deeply $t->app->basic_auth(bif => "Bif's Passwerdd"), {Authorization => 'Basic YmlmOkJpZidzIFBhc3N3ZXJkZA=='},
297 'correct header value';
298
299 Any aspect of the application (helpers, plugins, routes, etc.) can be
300 introspected from Test::Mojo through the application object. This
301 enables us to get deep test coverage of Mojolicious-based applications.
302
304 This section describes the basic test assertions supplied by
305 Test::Mojo. There are four broad categories of assertions for HTTP
306 requests:
307
308 • HTTP requests
309
310 • HTTP response status
311
312 • HTTP response headers
313
314 • HTTP response content/body
315
316 WebSocket test assertions are covered in "Testing WebSocket web
317 services".
318
319 HTTP request assertions
320 Test::Mojo has a Mojo::UserAgent object that allows it to make HTTP
321 requests and check for HTTP transport errors. HTTP request assertions
322 include "get_ok", "post_ok", etc. These assertions do not test whether
323 the request was handled successfully, only that the web application
324 handled the request in an HTTP compliant way.
325
326 You may also make HTTP requests using custom verbs (beyond "GET",
327 "POST", "PUT", etc.) by building your own transaction object. See
328 "Custom transactions" below.
329
330 Using HTTP request assertions
331
332 To post a URL-encoded form to the "/calls" endpoint of an application,
333 we simply use the "form" content type shortcut:
334
335 $t->post_ok('/calls' => form => {to => '+43.55.555.5555'});
336
337 Which will create the following HTTP request:
338
339 POST /calls HTTP/1.1
340 Content-Length: 20
341 Content-Type: application/x-www-form-urlencoded
342
343 to=%2B43.55.555.5555
344
345 The *_ok HTTP request assertion methods accept the same arguments as
346 their corresponding Mojo::UserAgent methods (except for the callback
347 argument). This allows us to set headers and build query strings for
348 authentic test situations:
349
350 $t->get_ok('/internal/personnel' => {Authorization => 'Token secret-password'} => form => {q => 'Professor Plum'});
351
352 which generates the following request:
353
354 GET /internal/personnel?q=Professor+Plum HTTP/1.1
355 Content-Length: 0
356 Authorization: Token secret-password
357
358 The "form" content generator (see Mojo::UserAgent::Transactor) will
359 generate a query string for "GET" requests and
360 "application/x-www-form-urlencoded" or "multipart/form-data" for POST
361 requests.
362
363 While these *_ok assertions make the HTTP requests we expect, they tell
364 us little about how well the application handled the request. The
365 application we're testing might have returned any content-type, body,
366 or HTTP status code (200, 302, 400, 404, 500, etc.) and we wouldn't
367 know it.
368
369 Test::Mojo provides assertions to test almost every aspect of the HTTP
370 response, including the HTTP response status code, the value of the
371 "Content-Type" header, and other arbitrary HTTP header information.
372
373 HTTP response status code
374 While not technically an HTTP header, the status line is the first line
375 in an HTTP response and is followed by the response headers. Testing
376 the response status code is common in REST-based and other web
377 applications that use the HTTP status codes to broadly indicate the
378 type of response the server is returning.
379
380 Testing the status code is as simple as adding the "status_is"
381 assertion:
382
383 $t->post_ok('/doorbell' => form => {action => 'ring once'})
384 ->status_is(200);
385
386 Along with "status_isnt", this will cover most needs. For more
387 elaborate status code testing, you can access the response internals
388 directly:
389
390 $t->post_ok('/doorbell' => form => {action => 'ring once'});
391 is $t->tx->res->message, 'Moved Permanently', 'try next door';
392
393 HTTP response headers
394 Test::Mojo allows us to inspect and make assertions about HTTP response
395 headers. The "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
407 chains:
408
409 $t->get_ok('/map-of-the-world.pdf')
410 ->content_type_is('application/pdf')
411 ->header_isnt('Compression' => 'gzip')
412 ->header_unlike('Server' => qr/IIS/i);
413
414 HTTP response content assertions
415 Test::Mojo also exposes a rich set of assertions for testing the body
416 of a response, whether that body be HTML, plain-text, or JSON. The
417 "content_*" methods look at the body of the response as plain text (as
418 defined by the 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, "content_is" treats it as if it were
424 a text document. This may be useful for situations where we're looking
425 for a particular string and not concerned with the structure of the
426 document. For example, we can 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 Test::Mojo has access to everything that Mojo::UserAgent
432 does, we can introspect JSON documents as well as DOM-based documents
433 (HTML, XML) with assertions that allow us to check for the existence of
434 elements as well as inspect the content of text nodes.
435
436 JSON response assertions
437
438 Test::Mojo's Mojo::UserAgent has access to a JSON parser, which allows
439 us to test to see if a JSON response contains a value at a location in
440 the document 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 "friendly.json" document contains a
446 value at the "/beings/jeremiah/age" JSON pointer location. We can also
447 inspect the value 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 DOM response assertions
457
458 We can also inspect HTML and XML responses using the Mojo::DOM parser
459 in the user agent. Here are a few examples from the Test::Mojo
460 documentation:
461
462 $t->text_is('div.foo[x=y]' => 'Hello!');
463 $t->text_is('html head title' => 'Hello!', 'right title');
464
465 The Mojo::DOM parser uses the CSS selector syntax described in
466 Mojo::DOM::CSS, allowing us to test for values in HTML and XML
467 documents without resorting to typically verbose and inflexible DOM
468 traversal methods.
469
471 This section describes some complex (but common) testing situations
472 that Test::Mojo excels in making simple.
473
474 Redirects
475 The Mojo::UserAgent object in Test::Mojo can handle HTTP redirections
476 internally to whatever level you need. Let's say we have a web service
477 that redirects "/1" to "/2", "/2" redirects to "/3", "/3" redirects to
478 "/4", and "/4" redirects to "/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 "/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 Test::Mojo how to deal with redirects.
507 Each test is making a request to "GET /1", but we vary the number of
508 redirects the 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 "max_redirects", it stays set for the life of the test
535 object until we change it.
536
537 Test::Mojo's handling of HTTP redirects eliminates the need for making
538 many, sometimes an unknown number, of redirections to keep testing
539 precise and easy to follow (ahem).
540
541 Cookies and session management
542 We can use Test::Mojo to test applications that keep session state in
543 cookies. By default, the Mojo::UserAgent object in Test::Mojo will
544 manage session for us by saving and sending cookies automatically, just
545 like common web browsers:
546
547 use Mojo::Base -strict;
548
549 use Test::More;
550 use Test::Mojo;
551
552 my $t = Test::Mojo->new('MyApp');
553
554 # No authorization cookie
555 $t->get_ok('/')
556 ->status_is(401)
557 ->content_is('Please log in');
558
559 # Application sets an authorization cookie
560 $t->post_ok('/login' => form => {password => 'let me in'})
561 ->status_is(200)
562 ->content_is('You are logged in');
563
564 # Sends the cookie from the previous transaction
565 $t->get_ok('/')
566 ->status_is(200)
567 ->content_like(qr/You logged in at \d+/);
568
569 # Clear the cookies
570 $t->reset_session;
571
572 # No authorization cookie again
573 $t->get_ok('/')
574 ->status_is(401)
575 ->content_is('Please log in');
576
577 We can also inspect cookies in responses for special values through the
578 transaction's response (Mojo::Message::Response) object:
579
580 $t->get_ok('/');
581 like $t->tx->res->cookie('smarty'), qr/smarty=pants/, 'cookie found';
582
583 Custom transactions
584 Let's say we have an application that responds to a new HTTP verb
585 "RING" and to use it we must also pass in a secret cookie value. This
586 is not a problem. We can test the application by creating a
587 Mojo::Transaction object, setting the cookie (see
588 Mojo::Message::Request), then passing the transaction object to
589 "request_ok":
590
591 # Use custom "RING" verb
592 my $tx = $t->ua->build_tx(RING => '/doorbell');
593
594 # Set a special cookie
595 $tx->req->cookies({name => 'Secret', value => "don't tell anybody"});
596
597 # Make the request
598 $t->request_ok($tx)
599 ->status_is(200)
600 ->json_is('/status' => 'ding dong');
601
602 Testing WebSocket web services
603 While the message flow on WebSocket connections can be rather dynamic,
604 it more often than not is quite predictable, which allows this rather
605 pleasant Test::Mojo WebSocket API to be used:
606
607 use Mojo::Base -strict;
608
609 use Test::More;
610 use Test::Mojo;
611
612 # Test echo web service
613 my $t = Test::Mojo->new('EchoService');
614 $t->websocket_ok('/echo')
615 ->send_ok('Hello Mojo!')
616 ->message_ok
617 ->message_is('echo: Hello Mojo!')
618 ->finish_ok;
619
620 # Test JSON web service
621 $t->websocket_ok('/echo.json')
622 ->send_ok({json => {test => [1, 2, 3]}})
623 ->message_ok
624 ->json_message_is('/test' => [1, 2, 3])
625 ->finish_ok;
626
627 done_testing();
628
629 Because of their inherent asynchronous nature, testing WebSocket
630 communications can be tricky. The Test::Mojo WebSocket assertions
631 serialize messages via event loop primitives. This enables us to treat
632 WebSocket messages as if they were using the same request-response
633 communication pattern we're accustomed to with HTTP.
634
635 To illustrate, let's walk through these tests. In the first test, we
636 use the "websocket_ok" assertion to ensure that we can connect to our
637 application's WebSocket route at "/echo" and that it's "speaking"
638 WebSocket protocol to us. The next "send_ok" assertion tests the
639 connection again (in case it closed, for example) and attempts to send
640 the message "Hello Mojo!". The next assertion, "message_ok", blocks
641 (using the Mojo::IOLoop singleton in the application) and waits for a
642 response from the server. The response is then compared with 'echo:
643 Hello Mojo!' in the "message_is" assertion, and finally we close and
644 test our connection status again with "finish_ok".
645
646 The second test is like the first, but now we're sending and expecting
647 JSON documents at "/echo.json". In the "send_ok" assertion we take
648 advantage of Mojo::UserAgent's JSON content generator (see
649 Mojo::UserAgent::Transactor) to marshal hash and array references into
650 JSON documents, and then send them as a WebSocket message. We wait
651 (block) for a response from the server with "message_ok". Then because
652 we're expecting a JSON document back, we can leverage "json_message_ok"
653 which parses the WebSocket response body and returns an object we can
654 access through Mojo::JSON::Pointer syntax. Then we close (and test) our
655 WebSocket connection.
656
657 Testing WebSocket servers does not get any simpler than with
658 Test::Mojo.
659
660 Extending Test::Mojo
661 If you see that you're writing a lot of test assertions that aren't
662 chainable, you may benefit from writing your own test assertions. Let's
663 say we want to test the "Location" header after a redirect. We'll
664 create a new class with Role::Tiny that implements a test assertion
665 named "location_is":
666
667 package Test::Mojo::Role::Location;
668 use Mojo::Base -role, -signatures;
669
670 sub location_is ($self, $value, $desc = "Location: $value") {
671 return $self->test('is', $self->tx->res->headers->location, $value, $desc);
672 }
673
674 1;
675
676 When we make new test assertions using roles, we want to use method
677 signatures that match other *_is methods in Test::Mojo, so here we
678 accept the test object, the value to compare, and an optional
679 description.
680
681 We assign a default description value ($desc), then we use "test" in
682 Test::Mojo to compare the location header with the expected header
683 value, and finally propagates the Test::Mojo object for method
684 chaining.
685
686 With this new package, we're ready to compose a new test object that
687 uses the role:
688
689 my $t = Test::Mojo->with_roles('+Location')->new('MyApp');
690
691 $t->post_ok('/redirect/mojo' => json => {message => 'Mojo, here I come!'})
692 ->status_is(302)
693 ->location_is('http://mojolicious.org')
694 ->or(sub { diag 'I miss tempire.' });
695
696 In this section we've covered how to add custom test assertions to
697 Test::Mojo with roles and how to use those roles to simplify testing.
698
700 You can continue with Mojolicious::Guides now or take a look at the
701 Mojolicious wiki <https://github.com/mojolicious/mojo/wiki>, which
702 contains a lot more documentation and examples by many different
703 authors.
704
706 If you have any questions the documentation might not yet answer, don't
707 hesitate to ask in the Forum <https://forum.mojolicious.org> or the
708 official IRC channel "#mojo" on "chat.freenode.net" (chat now!
709 <https://webchat.freenode.net/#mojo>).
710
711
712
713perl v5.32.1 2021-02-07 Mojolicious::Guides::Testing(3)