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 To test a Mojolicious::Lite application, pass the file path to the
171 application script to the constructor.
172
173 # Load application script relative to the "t" directory
174 use Mojo::File qw(curfile);
175 my $t = Test::Mojo->new(curfile->dirname->sibling('myapp.pl'));
176
177 This object initializes a Mojo::UserAgent object, loads the Mojolicious
178 application "Frogs", binds and listens on a free TCP port (e.g.,
179 32114), and starts the application event loop. When the Test::Mojo
180 object ($t) goes out of scope, the application is stopped.
181
182 Relative URLs in the test object method assertions ("get_ok",
183 "post_ok", etc.) will be sent to the Mojolicious application started by
184 Test::Mojo:
185
186 # Rewritten to "http://localhost:32114/frogs"
187 $t->get_ok('/frogs');
188
189 Test::Mojo has a lot of handy shortcuts built into it to make testing
190 Mojolicious or Mojolicious::Lite applications enjoyable.
191
192 An example
193
194 Let's spin up a Mojolicious application using "mojo generate app
195 MyApp". The "mojo" utility will create a working application and a "t"
196 directory with a working test file:
197
198 $ mojo generate app MyApp
199 [mkdir] /my_app/script
200 [write] /my_app/script/my_app
201 [chmod] /my_app/script/my_app 744
202 ...
203 [mkdir] /my_app/t
204 [write] /my_app/t/basic.t
205 ...
206
207 Let's run the tests (we'll create the "log" directory to quiet the
208 application output):
209
210 $ cd my_app
211 $ mkdir log
212 $ prove -lv t
213 t/basic.t ..
214 ok 1 - GET /
215 ok 2 - 200 OK
216 ok 3 - content is similar
217 1..3
218 ok
219 All tests successful.
220 Files=1, Tests=3, 0 wallclock secs ( 0.03 usr 0.01 sys + 0.33 cusr 0.07 csys = 0.44 CPU)
221 Result: PASS
222
223 The boilerplate test file looks like this:
224
225 use Mojo::Base -strict;
226
227 use Test::More;
228 use Test::Mojo;
229
230 my $t = Test::Mojo->new('MyApp');
231 $t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i);
232
233 done_testing();
234
235 Here we can see our application class name "MyApp" is passed to the
236 Test::Mojo constructor. Under the hood, Test::Mojo creates a new
237 Mojo::Server instance, loads "MyApp" (which we just created), and runs
238 the application. We write our tests with relative URLs because
239 Test::Mojo takes care of getting the request to the running test
240 application (since its port may change between runs).
241
242 Testing with configuration data
243
244 We can alter the behavior of our application using environment
245 variables (such as "MOJO_MODE") and through configuration values. One
246 nice feature of Test::Mojo is its ability to pass configuration values
247 directly from its constructor.
248
249 Let's modify our application and add a "feature flag" to enable a new
250 feature when the "enable_weather" configuration value is set:
251
252 # Load configuration from hash returned by "my_app.conf"
253 my $config = $self->plugin('Config');
254
255 # Normal route to controller
256 $r->get('/')->to('example#welcome');
257
258 # NEW: this route only exists if "enable_weather" is set in the configuration
259 if ($config->{enable_weather}) {
260 $r->get('/weather' => sub ($c) {
261 $c->render(text => "It's hot! 🔥");
262 });
263 }
264
265 To test this new feature, we don't even need to create a configuration
266 file—we can simply pass the configuration data to the application
267 directly via Test::Mojo's constructor:
268
269 my $t = Test::Mojo->new(MyApp => {enable_weather => 1});
270 $t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i);
271 $t->get_ok('/weather')->status_is(200)->content_like(qr/🔥/);
272
273 When we run these tests, Test::Mojo will pass this configuration data
274 to the application, which will cause it to create a special "/weather"
275 route that we can access in our tests. Unless "enable_weather" is set
276 in a configuration file, this route will not exist when the application
277 runs. Feature flags like this allow us to do soft rollouts of features,
278 targeting a small audience for a period of time. Once the feature has
279 been proven, we can refactor the conditional and make it a full
280 release.
281
282 This example shows how easy it is to start testing a Mojolicious
283 application and how to set specific application configuration
284 directives from a test file.
285
286 Testing application helpers
287
288 Let's say we register a helper in our application to generate an HTTP
289 Basic Authorization header:
290
291 use Mojo::Util qw(b64_encode);
292
293 app->helper(basic_auth => sub ($c, @values) {
294 return {Authorization => 'Basic ' . b64_encode join(':' => @values), ''};
295 });
296
297 How do we test application helpers like this? Test::Mojo has access to
298 the application object, which allows us to invoke helpers from our test
299 file:
300
301 my $t = Test::Mojo->new('MyApp');
302
303 is_deeply $t->app->basic_auth(bif => "Bif's Passwerdd"), {Authorization => 'Basic YmlmOkJpZidzIFBhc3N3ZXJkZA=='},
304 'correct header value';
305
306 Any aspect of the application (helpers, plugins, routes, etc.) can be
307 introspected from Test::Mojo through the application object. This
308 enables us to get deep test coverage of Mojolicious-based applications.
309
311 This section describes the basic test assertions supplied by
312 Test::Mojo. There are four broad categories of assertions for HTTP
313 requests:
314
315 • HTTP requests
316
317 • HTTP response status
318
319 • HTTP response headers
320
321 • HTTP response content/body
322
323 WebSocket test assertions are covered in "Testing WebSocket web
324 services".
325
326 HTTP request assertions
327 Test::Mojo has a Mojo::UserAgent object that allows it to make HTTP
328 requests and check for HTTP transport errors. HTTP request assertions
329 include "get_ok", "post_ok", etc. These assertions do not test whether
330 the request was handled successfully, only that the web application
331 handled the request in an HTTP compliant way.
332
333 You may also make HTTP requests using custom verbs (beyond "GET",
334 "POST", "PUT", etc.) by building your own transaction object. See
335 "Custom transactions" below.
336
337 Using HTTP request assertions
338
339 To post a URL-encoded form to the "/calls" endpoint of an application,
340 we simply use the "form" content type shortcut:
341
342 $t->post_ok('/calls' => form => {to => '+43.55.555.5555'});
343
344 Which will create the following HTTP request:
345
346 POST /calls HTTP/1.1
347 Content-Length: 20
348 Content-Type: application/x-www-form-urlencoded
349
350 to=%2B43.55.555.5555
351
352 The *_ok HTTP request assertion methods accept the same arguments as
353 their corresponding Mojo::UserAgent methods (except for the callback
354 argument). This allows us to set headers and build query strings for
355 authentic test situations:
356
357 $t->get_ok('/internal/personnel' => {Authorization => 'Token secret-password'} => form => {q => 'Professor Plum'});
358
359 which generates the following request:
360
361 GET /internal/personnel?q=Professor+Plum HTTP/1.1
362 Content-Length: 0
363 Authorization: Token secret-password
364
365 The "form" content generator (see Mojo::UserAgent::Transactor) will
366 generate a query string for "GET" requests and
367 "application/x-www-form-urlencoded" or "multipart/form-data" for POST
368 requests.
369
370 While these *_ok assertions make the HTTP requests we expect, they tell
371 us little about how well the application handled the request. The
372 application we're testing might have returned any content-type, body,
373 or HTTP status code (200, 302, 400, 404, 500, etc.) and we wouldn't
374 know it.
375
376 Test::Mojo provides assertions to test almost every aspect of the HTTP
377 response, including the HTTP response status code, the value of the
378 "Content-Type" header, and other arbitrary HTTP header information.
379
380 HTTP response status code
381 While not technically an HTTP header, the status line is the first line
382 in an HTTP response and is followed by the response headers. Testing
383 the response status code is common in REST-based and other web
384 applications that use the HTTP status codes to broadly indicate the
385 type of response the server is returning.
386
387 Testing the status code is as simple as adding the "status_is"
388 assertion:
389
390 $t->post_ok('/doorbell' => form => {action => 'ring once'})
391 ->status_is(200);
392
393 Along with "status_isnt", this will cover most needs. For more
394 elaborate status code testing, you can access the response internals
395 directly:
396
397 $t->post_ok('/doorbell' => form => {action => 'ring once'});
398 is $t->tx->res->message, 'Moved Permanently', 'try next door';
399
400 HTTP response headers
401 Test::Mojo allows us to inspect and make assertions about HTTP response
402 headers. The "Content-Type" header is commonly tested and has its own
403 assertion:
404
405 $t->get_ok('/map-of-the-world.pdf')
406 ->content_type_is('application/pdf');
407
408 This is equivalent to the more verbose:
409
410 $t->get_ok('/map-of-the-world.pdf')
411 ->header_is('Content-Type' => 'application/pdf');
412
413 We can test for multiple headers in a single response using method
414 chains:
415
416 $t->get_ok('/map-of-the-world.pdf')
417 ->content_type_is('application/pdf')
418 ->header_isnt('Compression' => 'gzip')
419 ->header_unlike('Server' => qr/IIS/i);
420
421 HTTP response content assertions
422 Test::Mojo also exposes a rich set of assertions for testing the body
423 of a response, whether that body be HTML, plain-text, or JSON. The
424 "content_*" methods look at the body of the response as plain text (as
425 defined by the response's character set):
426
427 $t->get_ok('/scary-things/spiders.json')
428 ->content_is('{"arachnid":"brown recluse"}');
429
430 Although this is a JSON document, "content_is" treats it as if it were
431 a text document. This may be useful for situations where we're looking
432 for a particular string and not concerned with the structure of the
433 document. For example, we can do the same thing with an HTML document:
434
435 $t->get_ok('/scary-things/spiders.html')
436 ->content_like(qr{<title>All The Spiders</title>});
437
438 But because Test::Mojo has access to everything that Mojo::UserAgent
439 does, we can introspect JSON documents as well as DOM-based documents
440 (HTML, XML) with assertions that allow us to check for the existence of
441 elements as well as inspect the content of text nodes.
442
443 JSON response assertions
444
445 Test::Mojo's Mojo::UserAgent has access to a JSON parser, which allows
446 us to test to see if a JSON response contains a value at a location in
447 the document using JSON pointer syntax:
448
449 $t->get_ok('/animals/friendly.json')
450 ->json_has('/beings/jeremiah/age');
451
452 This assertion tells us that the "friendly.json" document contains a
453 value at the "/beings/jeremiah/age" JSON pointer location. We can also
454 inspect the value at JSON pointer locations:
455
456 $t->get_ok('/animals/friendly.json')
457 ->json_has('/beings/jeremiah/age')
458 ->json_is('/beings/jeremiah/age' => 42)
459 ->json_like('/beings/jeremiah/species' => qr/bullfrog/i);
460
461 JSON pointer syntax makes testing JSON responses simple and readable.
462
463 DOM response assertions
464
465 We can also inspect HTML and XML responses using the Mojo::DOM parser
466 in the user agent. Here are a few examples from the Test::Mojo
467 documentation:
468
469 $t->text_is('div.foo[x=y]' => 'Hello!');
470 $t->text_is('html head title' => 'Hello!', 'right title');
471
472 The Mojo::DOM parser uses the CSS selector syntax described in
473 Mojo::DOM::CSS, allowing us to test for values in HTML and XML
474 documents without resorting to typically verbose and inflexible DOM
475 traversal methods.
476
478 This section describes some complex (but common) testing situations
479 that Test::Mojo excels in making simple.
480
481 Redirects
482 The Mojo::UserAgent object in Test::Mojo can handle HTTP redirections
483 internally to whatever level you need. Let's say we have a web service
484 that redirects "/1" to "/2", "/2" redirects to "/3", "/3" redirects to
485 "/4", and "/4" redirects to "/5":
486
487 GET /1
488
489 returns:
490
491 302 Found
492 Location: /2
493
494 and:
495
496 GET /2
497
498 returns:
499
500 302 Found
501 Location: /3
502
503 and so forth, up to "/5":
504
505 GET /5
506
507 which returns the data we wanted:
508
509 200 OK
510
511 {"message":"this is five"}
512
513 We can tell the user agent in Test::Mojo how to deal with redirects.
514 Each test is making a request to "GET /1", but we vary the number of
515 redirects the user agent should follow with each test:
516
517 my $t = Test::Mojo->new;
518
519 $t->get_ok('/1')
520 ->header_is(location => '/2');
521
522 $t->ua->max_redirects(1);
523 $t->get_ok('/1')
524 ->header_is(location => '/3');
525
526 $t->ua->max_redirects(2);
527 $t->get_ok('/1')
528 ->header_is(location => '/4');
529
530 # Look at the previous hop
531 is $t->tx->previous->res->headers->location, '/3', 'previous redirect';
532
533 $t->ua->max_redirects(3);
534 $t->get_ok('/1')
535 ->header_is(location => '/5');
536
537 $t->ua->max_redirects(4);
538 $t->get_ok('/1')
539 ->json_is('/message' => 'this is five');
540
541 When we set "max_redirects", it stays set for the life of the test
542 object until we change it.
543
544 Test::Mojo's handling of HTTP redirects eliminates the need for making
545 many, sometimes an unknown number, of redirections to keep testing
546 precise and easy to follow (ahem).
547
548 Cookies and session management
549 We can use Test::Mojo to test applications that keep session state in
550 cookies. By default, the Mojo::UserAgent object in Test::Mojo will
551 manage session for us by saving and sending cookies automatically, just
552 like common web browsers:
553
554 use Mojo::Base -strict;
555
556 use Test::More;
557 use Test::Mojo;
558
559 my $t = Test::Mojo->new('MyApp');
560
561 # No authorization cookie
562 $t->get_ok('/')
563 ->status_is(401)
564 ->content_is('Please log in');
565
566 # Application sets an authorization cookie
567 $t->post_ok('/login' => form => {password => 'let me in'})
568 ->status_is(200)
569 ->content_is('You are logged in');
570
571 # Sends the cookie from the previous transaction
572 $t->get_ok('/')
573 ->status_is(200)
574 ->content_like(qr/You logged in at \d+/);
575
576 # Clear the cookies
577 $t->reset_session;
578
579 # No authorization cookie again
580 $t->get_ok('/')
581 ->status_is(401)
582 ->content_is('Please log in');
583
584 We can also inspect cookies in responses for special values through the
585 transaction's response (Mojo::Message::Response) object:
586
587 $t->get_ok('/');
588 like $t->tx->res->cookie('smarty'), qr/smarty=pants/, 'cookie found';
589
590 Custom transactions
591 Let's say we have an application that responds to a new HTTP verb
592 "RING" and to use it we must also pass in a secret cookie value. This
593 is not a problem. We can test the application by creating a
594 Mojo::Transaction object, setting the cookie (see
595 Mojo::Message::Request), then passing the transaction object to
596 "request_ok":
597
598 # Use custom "RING" verb
599 my $tx = $t->ua->build_tx(RING => '/doorbell');
600
601 # Set a special cookie
602 $tx->req->cookies({name => 'Secret', value => "don't tell anybody"});
603
604 # Make the request
605 $t->request_ok($tx)
606 ->status_is(200)
607 ->json_is('/status' => 'ding dong');
608
609 Testing WebSocket web services
610 While the message flow on WebSocket connections can be rather dynamic,
611 it more often than not is quite predictable, which allows this rather
612 pleasant Test::Mojo WebSocket API to be used:
613
614 use Mojo::Base -strict;
615
616 use Test::More;
617 use Test::Mojo;
618
619 # Test echo web service
620 my $t = Test::Mojo->new('EchoService');
621 $t->websocket_ok('/echo')
622 ->send_ok('Hello Mojo!')
623 ->message_ok
624 ->message_is('echo: Hello Mojo!')
625 ->finish_ok;
626
627 # Test JSON web service
628 $t->websocket_ok('/echo.json')
629 ->send_ok({json => {test => [1, 2, 3]}})
630 ->message_ok
631 ->json_message_is('/test' => [1, 2, 3])
632 ->finish_ok;
633
634 done_testing();
635
636 Because of their inherent asynchronous nature, testing WebSocket
637 communications can be tricky. The Test::Mojo WebSocket assertions
638 serialize messages via event loop primitives. This enables us to treat
639 WebSocket messages as if they were using the same request-response
640 communication pattern we're accustomed to with HTTP.
641
642 To illustrate, let's walk through these tests. In the first test, we
643 use the "websocket_ok" assertion to ensure that we can connect to our
644 application's WebSocket route at "/echo" and that it's "speaking"
645 WebSocket protocol to us. The next "send_ok" assertion tests the
646 connection again (in case it closed, for example) and attempts to send
647 the message "Hello Mojo!". The next assertion, "message_ok", blocks
648 (using the Mojo::IOLoop singleton in the application) and waits for a
649 response from the server. The response is then compared with 'echo:
650 Hello Mojo!' in the "message_is" assertion, and finally we close and
651 test our connection status again with "finish_ok".
652
653 The second test is like the first, but now we're sending and expecting
654 JSON documents at "/echo.json". In the "send_ok" assertion we take
655 advantage of Mojo::UserAgent's JSON content generator (see
656 Mojo::UserAgent::Transactor) to marshal hash and array references into
657 JSON documents, and then send them as a WebSocket message. We wait
658 (block) for a response from the server with "message_ok". Then because
659 we're expecting a JSON document back, we can leverage "json_message_ok"
660 which parses the WebSocket response body and returns an object we can
661 access through Mojo::JSON::Pointer syntax. Then we close (and test) our
662 WebSocket connection.
663
664 Testing WebSocket servers does not get any simpler than with
665 Test::Mojo.
666
667 Extending Test::Mojo
668 If you see that you're writing a lot of test assertions that aren't
669 chainable, you may benefit from writing your own test assertions. Let's
670 say we want to test the "Location" header after a redirect. We'll
671 create a new class with Role::Tiny that implements a test assertion
672 named "location_is":
673
674 package Test::Mojo::Role::Location;
675 use Mojo::Base -role, -signatures;
676
677 sub location_is ($self, $value, $desc = "Location: $value") {
678 return $self->test('is', $self->tx->res->headers->location, $value, $desc);
679 }
680
681 1;
682
683 When we make new test assertions using roles, we want to use method
684 signatures that match other *_is methods in Test::Mojo, so here we
685 accept the test object, the value to compare, and an optional
686 description.
687
688 We assign a default description value ($desc), then we use "test" in
689 Test::Mojo to compare the location header with the expected header
690 value, and finally propagates the Test::Mojo object for method
691 chaining.
692
693 With this new package, we're ready to compose a new test object that
694 uses the role:
695
696 my $t = Test::Mojo->with_roles('+Location')->new('MyApp');
697
698 $t->post_ok('/redirect/mojo' => json => {message => 'Mojo, here I come!'})
699 ->status_is(302)
700 ->location_is('http://mojolicious.org')
701 ->or(sub { diag 'I miss tempire.' });
702
703 In this section we've covered how to add custom test assertions to
704 Test::Mojo with roles and how to use those roles to simplify testing.
705
707 You can continue with Mojolicious::Guides now or take a look at the
708 Mojolicious wiki <https://github.com/mojolicious/mojo/wiki>, which
709 contains a lot more documentation and examples by many different
710 authors.
711
713 If you have any questions the documentation might not yet answer, don't
714 hesitate to ask in the Forum <https://forum.mojolicious.org> or the
715 official IRC channel "#mojo" on "irc.libera.chat" (chat now!
716 <https://web.libera.chat/#mojo>).
717
718
719
720perl v5.34.0 2021-07-22 Mojolicious::Guides::Testing(3)