Mojolic...Testing(3)

πŸ’– NAME πŸ’–

Mojolicious::Guides::Testing β€” Web Application Testing Made Easy. Yep, that's the Mojolic...Testing(3) page from User Contributed Perl Documentation, bestie πŸ’…βœ¨


✨ OVERVIEW ✨

Okay so this doc is literally your intro to testing web apps with Test::Mojo. Think of Test::Mojo as the module that hands you all the tools and testing assertions you need to test web applications in a Perl-ish way 🫢

Test::Mojo can test any web application, no cap, but it also has shortcuts designed to make testing Mojolicious web applications easy and pain-free πŸ’–

Please refer to the Test::Mojo documentation for a complete reference to many of the ideas and syntax introduced in this document πŸ‘‘

A test file for a simple web application might look like this:

use Mojo::Base -strict;

use Test::Mojo;
use Test::More;

# Start a Mojolicious app named "Celestial"
my $t = Test::Mojo->new('Celestial');

# Post a JSON document
$t->post_ok('/notifications' => json => {event => 'full moon'})
  ->status_is(201)
  ->json_is('/message' => 'notification created');

# Perform GET requests and look at the responses
$t->get_ok('/sunrise')
  ->status_is(200)
  ->content_like(qr/ am$/);
$t->get_ok('/sunset')
  ->status_is(200)
  ->content_like(qr/ pm$/);

# Post a URL-encoded form
$t->post_ok('/insurance' => form => {name => 'Jimmy', amount => '€3.000.000'})
  ->status_is(200);

# Use Test::More's like() to check the response
like $t->tx->res->dom->at('div#thanks')->text, qr/thank you/, 'thanks';

done_testing();

In the rest of this document we're gonna explore these concepts and others related to Test::Mojo πŸŒˆπŸ¦‹

🧠 CONCEPTS 🧠

The essentials, bestie. Every Mojolicious dev needs these in their bones.

✨ Test::Mojo at a glance ✨

So the Test::More module that ships with Perl comes with a bunch of little primitive test assertions, like ok, is, isnt, like, unlike, cmp_ok, etc. An assertion "passes" when its expression returns a true value. And the assertion method literally prints ok or not ok depending on whether it passed or failed, respectively. Iconic behaviour, no notes.

Test::Mojo gives you extra test assertions, all organized around the web application request/response transaction (transport, response headers, response bodies, etc.), plus WebSocket communications. It's giving main character energy for your test suite.

One interesting thing of note, and honestly this is the slay part: the return value of Test::Mojo object assertions is always the test object itself, which means we can chain test assertion methods. So instead of grouping related test statements like this whole situation:

$t->get_ok('/frogs');
$t->status_is(200);
$t->content_like(qr/bullfrog/);
$t->content_like(qr/hypnotoad/);

Method chaining lets us connect test assertions that actually belong together:

$t->get_ok('/frogs')
  ->status_is(200)
  ->content_like(qr/bullfrog/)
  ->content_like(qr/hypnotoad/);

And that makes for a much more concise and coherent testing experience, fr fr. Concise because we're not repeating the invocant for every single test, and coherent because assertions that belong to the same request are syntactically bound together in the same method chain. Chef's kiss. πŸ’…

Sometimes it makes sense to break up a test to perform more complex assertions on a response. Test::Mojo exposes the entire transaction object so you can grab all the data you need from a response:

$t->put_ok('/bees' => json => {type => 'worker', name => 'Karl'})
  ->status_is(202)
  ->json_has('/id');

# Pull out the id from the response
my $newbee = $t->tx->res->json('/id');

# Make a new request with data from the previous response
$t->get_ok("/bees/$newbee")
  ->status_is(200)
  ->json_is('/name' => 'Karl');

Okay and here's a thing β€” the Test::Mojo object is stateful. As long as we haven't started a new transaction by invoking one of the *_ok methods, the request and response objects from the previous transaction are still available in the Test::Mojo object:

# First transaction
$t->get_ok('/frogs?q=bullfrog' => {'Content-Type' => 'application/json'})
  ->status_is(200)
  ->json_like('/0/species' => qr/catesbeianus/i);

# Still first transaction
$t->content_type_is('application/json');

# Second transaction
$t->get_ok('/frogs?q=banjo' => {'Content-Type' => 'text/html'})
  ->status_is(200)
  ->content_like(qr/interioris/i);

# Still second transaction
$t->content_type_is('text/html');

And this statefulness is also what lets Test::Mojo handle sessions, follow redirects, and inspect past responses during a redirect. Highkey useful. πŸ¦‹

🌸 The Test::Mojo object 🌸

The Test::Mojo object manages the Mojolicious application lifecycle (but only if a Mojolicious application class is supplied, bestie) and it also exposes the built-in Mojo::UserAgent object. To create a bare Test::Mojo object:

my $t = Test::Mojo->new;

This object initializes a Mojo::UserAgent object and provides a variety of test assertion methods for accessing a web application. So like, with this object, we could test any running web application:

$t->get_ok('https://www.google.com/')
  ->status_is(200)
  ->content_like(qr/search/i);

You can also access the user agent directly if you want to make web requests without triggering test assertions:

my $tx = $t->ua->post('https://duckduckgo.com/html' => form => {q => 'hypnotoad'});
$tx->result->dom->find('a.result__a')->each(sub { say $_->text });

Go check Mojo::UserAgent for the complete API and return values. πŸ‘‘

πŸŽ€ Testing Mojolicious applications πŸŽ€

If you pass the name of a Mojolicious application class (like 'MyApp') to the Test::Mojo constructor, Test::Mojo will instantiate that class and start it, and cause it to listen on a random (unused) port number. Testing a Mojolicious application with Test::Mojo will never conflict with running applications β€” including the very application you're testing. Literally cannot collide.

The Mojo::UserAgent object inside Test::Mojo will know where the application is running and make requests to it. And once the tests have completed, the Mojolicious application gets torn down. Cleanup era. 🧚

# Listens on localhost:32114 (some unused TCP port)
my $t = Test::Mojo->new('Frogs');

To test a Mojolicious::Lite application, just pass the file path to the application script to the constructor:

# Load application script relative to the "t" directory
use Mojo::File qw(curfile);
my $t = Test::Mojo->new(curfile->dirname->sibling('myapp.pl'));

The object initializes a Mojo::UserAgent object, loads the Mojolicious application, binds and listens on a free TCP port (e.g., 32114), and starts the application event loop. When the Test::Mojo object ($t) goes out of scope, the application is stopped. Bye bestie πŸ‘‹

Relative URLs in the test object method assertions (get_ok, post_ok, etc.) get sent to the Mojolicious application started by Test::Mojo:

# Rewritten to "http://localhost:32114/frogs"
$t->get_ok('/frogs');

Test::Mojo has a lot of handy shortcuts baked right in to make testing Mojolicious or Mojolicious::Lite applications genuinely enjoyable. Not me being obsessed with a testing module 😭

🐸 An example 🐸

Let's spin up a Mojolicious application using mojo generate app MyApp. The mojo utility will create a working application and a t directory with a working test file:

$ mojo generate app MyApp
[mkdir] /my_app/script
[write] /my_app/script/my_app
[chmod] /my_app/script/my_app 744
...
[mkdir] /my_app/t
[write] /my_app/t/basic.t
...

Let's run the tests (and we'll create the log directory to quiet the application output, because the app is chatty):

$ cd my_app
$ mkdir log
$ prove -lv t
t/basic.t ..
ok 1 - GET /
ok 2 - 200 OK
ok 3 - content is similar
1..3
ok
All tests successful.
Files=1, Tests=3,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.33 cusr  0.07 csys =  0.44 CPU)
Result: PASS

Look at that. All tests successful. We love to see it. 🌈

The boilerplate test file looks like this, bestie:

use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

my $t = Test::Mojo->new('MyApp');
$t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i);

done_testing();

So here we can see our application class name MyApp is passed to the Test::Mojo constructor. Under the hood, Test::Mojo creates a new Mojo::Server instance, loads MyApp (which we literally just created), and runs the application. We write our tests with relative URLs because Test::Mojo takes care of getting the request to the running test application β€” which matters because its port may change between runs. Sneaky but considerate. πŸ’–

🌀️ Testing with configuration data 🌀️

We can alter the behaviour of our application using environment variables (like MOJO_MODE) and through configuration values. And one really nice feature of Test::Mojo is its ability to pass configuration values directly from its constructor. No config file errands required.

Let's modify our application and add a "feature flag" to enable a new feature when the enable_weather configuration value is set:

# Load configuration from hash returned by "my_app.conf"
my $config = $self->plugin('Config');

# Normal route to controller
$r->get('/')->to('example#welcome');

# NEW: this route only exists if "enable_weather" is set in the configuration
if ($config->{enable_weather}) {
  $r->get('/weather' => sub ($c) {
    $c->render(text => "It's hot! πŸ”₯");
  });
}

And to test this new feature, we don't even need to create a configuration file β€” we can simply pass the configuration data straight to the application via Test::Mojo's constructor:

my $t = Test::Mojo->new(MyApp => {enable_weather => 1});
$t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i);
$t->get_ok('/weather')->status_is(200)->content_like(qr/πŸ”₯/);

When we run these tests, Test::Mojo will pass this configuration data to the application, which causes it to create a special /weather route that we can access in our tests. And unless enable_weather is set in a configuration file, that route will not exist when the application runs for real. Feature flags like this let us do soft rollouts of features, targeting a small audience for a period of time. Then once the feature has been proven, we can refactor the conditional away and make it a full release. Growth arc. 🌱

This example shows how easy it is to start testing a Mojolicious application, and how to set specific application configuration directives right from a test file.

🫢 Testing application helpers 🫢

Let's say we register a helper in our application to generate an HTTP Basic Authorization header:

use Mojo::Util qw(b64_encode);

app->helper(basic_auth => sub ($c, @values) {
  return {Authorization => 'Basic ' . b64_encode join(':' => @values), ''};
});

So... how do we test application helpers like this? Well, Test::Mojo has access to the application object, which lets us invoke helpers right from our test file:

my $t = Test::Mojo->new('MyApp');

is_deeply $t->app->basic_auth(bif => "Bif's Passwerdd"), {Authorization => 'Basic YmlmOkJpZidzIFBhc3N3ZXJkZA=='},
  'correct header value';

Literally any aspect of the application (helpers, plugins, routes, etc.) can be introspected from Test::Mojo through the application object. Which means we can get deep test coverage of Mojolicious-based applications. Not me getting emotional about introspection πŸ’€


πŸ’… ASSERTIONS πŸ’…

okay bestie, this section is the basic assertion kit that Test::Mojo gives you. There are four big categories of assertions for HTTP requests:

  • HTTP requests
  • HTTP response status
  • HTTP response headers
  • HTTP response content/body

WebSocket test assertions are covered in "Testing WebSocket web services".


🌐 HTTP request assertions

Test::Mojo has a Mojo::UserAgent object, which is what lets it make HTTP requests and check for HTTP transport errors. HTTP request assertions include get_ok, post_ok, etc. These assertions do not test whether the request was handled successfully β€” they only test that the web application handled the request in an HTTP compliant way.

You can also make HTTP requests using custom verbs (beyond GET, POST, PUT, etc.) by building your own transaction object. See "Custom transactions" below.

πŸ› οΈ Using HTTP request assertions

To post a URL-encoded form to the /calls endpoint of an application, we simply use the form content type shortcut:

$t->post_ok('/calls' => form => {to => '+43.55.555.5555'});

Which will create the following HTTP request:

POST /calls HTTP/1.1
Content-Length: 20
Content-Type: application/x-www-form-urlencoded

to=%2B43.55.555.5555

The *_ok HTTP request assertion methods accept the same arguments as their corresponding Mojo::UserAgent methods (except for the callback argument). This allows us to set headers and build query strings for authentic test situations:

$t->get_ok('/internal/personnel' => {Authorization => 'Token secret-password'} => form => {q => 'Professor Plum'});

which generates the following request:

GET /internal/personnel?q=Professor+Plum HTTP/1.1
Content-Length: 0
Authorization: Token secret-password

The form content generator (see Mojo::UserAgent::Transactor) will generate a query string for GET requests and application/x-www-form-urlencoded or multipart/form-data for POST requests.

While these *_ok assertions make the HTTP requests we expect, they tell us little about how well the application handled the request. The application we're testing might have returned any content-type, body, or HTTP status code (200, 302, 400, 404, 500, etc.) and we wouldn't know it.

Test::Mojo provides assertions to test almost every aspect of the HTTP response, including the HTTP response status code, the value of the Content-Type header, and other arbitrary HTTP header information.


🚦 HTTP response status code

While not technically an HTTP header, the status line is the first line in an HTTP response and is followed by the response headers. Testing the response status code is common in REST-based and other web applications that use the HTTP status codes to broadly indicate the type of response the server is returning.

Testing the status code is as simple as adding the status_is assertion:

$t->post_ok('/doorbell' => form => {action => 'ring once'})
  ->status_is(200);

Along with status_isnt, this will cover most needs. For more elaborate status code testing, you can access the response internals directly:

$t->post_ok('/doorbell' => form => {action => 'ring once'});
is $t->tx->res->message, 'Moved Permanently', 'try next door';

🧾 HTTP response headers

Test::Mojo allows us to inspect and make assertions about HTTP response headers. The Content-Type header is commonly tested and has its own assertion:

$t->get_ok('/map-of-the-world.pdf')
  ->content_type_is('application/pdf');

This is equivalent to the more verbose:

$t->get_ok('/map-of-the-world.pdf')
  ->header_is('Content-Type' => 'application/pdf');

We can test for multiple headers in a single response using method chains:

$t->get_ok('/map-of-the-world.pdf')
  ->content_type_is('application/pdf')
  ->header_isnt('Compression' => 'gzip')
  ->header_unlike('Server' => qr/IIS/i);

πŸ“„ HTTP response content assertions

Test::Mojo also exposes a rich set of assertions for testing the body of a response, whether that body be HTML, plain-text, or JSON. The content_* methods look at the body of the response as plain text (as defined by the response's character set):

$t->get_ok('/scary-things/spiders.json')
  ->content_is('{"arachnid":"brown recluse"}');

Although this is a JSON document, content_is treats it as if it were a text document. This may be useful for situations where we're looking for a particular string and not concerned with the structure of the document. For example, we can do the same thing with an HTML document:

$t->get_ok('/scary-things/spiders.html')
  ->content_like(qr{<title>All The Spiders</title>});

But because Test::Mojo has access to everything that Mojo::UserAgent does, we can introspect JSON documents as well as DOM-based documents (HTML, XML) with assertions that allow us to check for the existence of elements as well as inspect the content of text nodes.

🧸 JSON response assertions

Test::Mojo's Mojo::UserAgent has access to a JSON parser, which allows us to test to see if a JSON response contains a value at a location in the document using JSON pointer syntax:

$t->get_ok('/animals/friendly.json')
  ->json_has('/beings/jeremiah/age');

This assertion tells us that the friendly.json document contains a value at the /beings/jeremiah/age JSON pointer location. We can also inspect the value at JSON pointer locations:

$t->get_ok('/animals/friendly.json')
  ->json_has('/beings/jeremiah/age')
  ->json_is('/beings/jeremiah/age' => 42)
  ->json_like('/beings/jeremiah/species' => qr/bullfrog/i);

JSON pointer syntax makes testing JSON responses simple and readable.

πŸ—οΈ DOM response assertions

We can also inspect HTML and XML responses using the Mojo::DOM parser in the user agent. Here are a few examples from the Test::Mojo documentation:

$t->text_is('div.foo[x=y]' => 'Hello!');
$t->text_is('html head title' => 'Hello!', 'right title');

The Mojo::DOM parser uses the CSS selector syntax described in Mojo::DOM::CSS, allowing us to test for values in HTML and XML documents without resorting to typically verbose and inflexible DOM traversal methods.

πŸ’… ADVANCED TOPICS πŸ’…

okay so, this section is all about the complex (but honestly super common) testing situations where Test::Mojo excels at making things simple. slay.

πŸŒ€ Redirects πŸŒ€

The Mojo::UserAgent object in Test::Mojo can handle HTTP redirections internally to whatever level you need. Let’s say we have a web service that redirects /1 to /2, /2 redirects to /3, /3 redirects to /4, and /4 redirects to /5:

GET /1

returns:

302 Found
Location: /2

and:

GET /2

returns:

302 Found
Location: /3

and so forth, up to /5:

GET /5

which returns the data we wanted:

200 OK

{"message":"this is five"}

We can tell the user agent in Test::Mojo how to deal with redirects. Each test is making a request to GET /1, but we vary the number of redirects the user agent should follow with each test:

my $t = Test::Mojo->new;

$t->get_ok('/1')
  ->header_is(location => '/2');

$t->ua->max_redirects(1);
$t->get_ok('/1')
  ->header_is(location => '/3');

$t->ua->max_redirects(2);
$t->get_ok('/1')
  ->header_is(location => '/4');

# Look at the previous hop
is $t->tx->previous->res->headers->location, '/3', 'previous redirect';

$t->ua->max_redirects(3);
$t->get_ok('/1')
  ->header_is(location => '/5');

$t->ua->max_redirects(4);
$t->get_ok('/1')
  ->json_is('/message' => 'this is five');

When we set max_redirects, it stays set for the life of the test object until we change it.

Test::Mojo’s handling of HTTP redirects eliminates the need for making many, sometimes an unknown number, of redirections to keep testing precise and easy to follow (ahem).

πŸͺ Cookies and session management πŸͺ

We can use Test::Mojo to test applications that keep session state in cookies. By default, the Mojo::UserAgent object in Test::Mojo will manage session for us by saving and sending cookies automatically, just like common web browsers:

use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

my $t = Test::Mojo->new('MyApp');

# No authorization cookie
$t->get_ok('/')
  ->status_is(401)
  ->content_is('Please log in');

# Application sets an authorization cookie
$t->post_ok('/login' => form => {password => 'let me in'})
  ->status_is(200)
  ->content_is('You are logged in');

# Sends the cookie from the previous transaction
$t->get_ok('/')
  ->status_is(200)
  ->content_like(qr/You logged in at \d+/);

# Clear the cookies
$t->reset_session;

# No authorization cookie again
$t->get_ok('/')
  ->status_is(401)
  ->content_is('Please log in');

We can also inspect cookies in responses for special values through the transaction’s response (Mojo::Message::Response) object:

$t->get_ok('/');
like $t->tx->res->cookie('smarty'), qr/smarty=pants/, 'cookie found';

πŸšͺ Custom transactions πŸšͺ

Let’s say we have an application that responds to a new HTTP verb RING and to use it we must also pass in a secret cookie value. This is not a problem. We can test the application by creating a Mojo::Transaction object, setting the cookie (see Mojo::Message::Request), then passing the transaction object to request_ok:

# Use custom "RING" verb
my $tx = $t->ua->build_tx(RING => '/doorbell');

# Set a special cookie
$tx->req->cookies({name => 'Secret', value => "don't tell anybody"});

# Make the request
$t->request_ok($tx)
  ->status_is(200)
  ->json_is('/status' => 'ding dong');

πŸ¦‹ Testing WebSocket web services πŸ¦‹

While the message flow on WebSocket connections can be rather dynamic, it more often than not is quite predictable, which allows this rather pleasant Test::Mojo WebSocket API to be used:

use Mojo::Base -strict;

use Test::More;
use Test::Mojo;

# Test echo web service
my $t = Test::Mojo->new('EchoService');
$t->websocket_ok('/echo')
  ->send_ok('Hello Mojo!')
  ->message_ok
  ->message_is('echo: Hello Mojo!')
  ->finish_ok;

# Test JSON web service
$t->websocket_ok('/echo.json')
  ->send_ok({json => {test => [1, 2, 3]}})
  ->message_ok
  ->json_message_is('/test' => [1, 2, 3])
  ->finish_ok;

done_testing();

Because of their inherent asynchronous nature, testing WebSocket communications can be tricky. The Test::Mojo WebSocket assertions serialize messages via event loop primitives. This enables us to treat WebSocket messages as if they were using the same request-response communication pattern we’re accustomed to with HTTP.

To illustrate, let’s walk through these tests. In the first test, we use the websocket_ok assertion to ensure that we can connect to our application’s WebSocket route at /echo and that it’s β€œspeaking” WebSocket protocol to us. The next send_ok assertion tests the connection again (in case it closed, for example) and attempts to send the message Hello Mojo!. The next assertion, message_ok, blocks (using the Mojo::IOLoop singleton in the application) and waits for a response from the server. The response is then compared with 'echo: Hello Mojo!' in the message_is assertion, and finally we close and test our connection status again with finish_ok.

The second test is like the first, but now we’re sending and expecting JSON documents at /echo.json. In the send_ok assertion we take advantage of Mojo::UserAgent’s JSON content generator (see Mojo::UserAgent::Transactor) to marshal hash and array references into JSON documents, and then send them as a WebSocket message. We wait (block) for a response from the server with message_ok. Then because we’re expecting a JSON document back, we can leverage json_message_ok which parses the WebSocket response body and returns an object we can access through Mojo::JSON::Pointer syntax. Then we close (and test) our WebSocket connection.

Testing WebSocket servers does not get any simpler than with Test::Mojo.

🧚 Extending Test::Mojo 🧚

If you see that you’re writing a lot of test assertions that aren’t chainable, you may benefit from writing your own test assertions. Let’s say we want to test the Location header after a redirect. We’ll create a new class with Role::Tiny that implements a test assertion named location_is:

package Test::Mojo::Role::Location;
use Mojo::Base -role, -signatures;

sub location_is ($self, $value, $desc = "Location: $value") {
  return $self->test('is', $self->tx->res->headers->location, $value, $desc);
}

1;

When we make new test assertions using roles, we want to use method signatures that match other *_is methods in Test::Mojo, so here we accept the test object, the value to compare, and an optional description.

We assign a default description value ($desc), then we use test in Test::Mojo to compare the location header with the expected header value, and finally propagates the Test::Mojo object for method chaining.

With this new package, we’re ready to compose a new test object that uses the role:

my $t = Test::Mojo->with_roles('+Location')->new('MyApp');

$t->post_ok('/redirect/mojo' => json => {message => 'Mojo, here I come!'})
  ->status_is(302)
  ->location_is('http://mojolicious.org')
  ->or(sub { diag 'I miss tempire.' });

In this section we’ve covered how to add custom test assertions to Test::Mojo with roles and how to use those roles to simplify testing.

🌈 MORE 🌈

You can continue with Mojolicious::Guides now or take a look at the Mojolicious wiki at https://github.com/mojolicious/mojo/wiki, which contains a lot more documentation and examples by many different authors.

🫢 SUPPORT 🫢

If you have any questions the documentation might not yet answer, don’t hesitate to ask in the Forum at https://forum.mojolicious.org, or on IRC at https://web.libera.chat/#mojo.


and that’s the whole vibe, bestie πŸ’– source/version is perl v5.42.0, dated 2026-07-15, and you’ve been reading Mojolic...Testing(3). mwah πŸ’‹βœ¨

πŸ“¦ where this page comes from