Using Mojolicious::Plugin::Mount to help test your applications
Introduction
When testing Perl applications we have many well known options and techniques but in this post we won't cover any of those fancy tools. Instead we will present a simple trick that takes advantage of the Mojolicious::Plugin::Mount.
Test Scenario
When testing modules or applications that make requests to third party services, one technique is to mock the user agent to answer mocked data for each request.
But when the module or application makes multiple requests to different external services that could be challenging.
The idea here is to mock the external services instead.
How Mojolicious::Plugin::Mount can help
This plugin basically allows us to glue together small Mojo applications into a single one that could be started by Test::Mojo.
Consider the two following small apps:
#!/usr/bin/env perl
use v5.42;
use Mojolicious::Lite -signatures;
get '/baz' => sub ($c) { $c->render( text => 'baz' ) };
get '/etc' => sub ($c) { $c->render( text => 'etc' ) };
app->start;
#!vim perl
#!/usr/bin/env perl
use v5.42;
use Mojolicious::Lite -signatures;
get '/lala' => sub ($c) { $c->render( text => 'lala' ) };
get '/lele' => sub ($c) { $c->render( text => 'lele' ) };
app->start;
You can check their routes by saving each one to separate files, making them executable, and running these commands:
user@host$ ./foo.pl routes
/baz GET baz
/etc GET etc
user@host$ ./bar.pl routes
/lala GET lala
/lele GET lele
By using Mojolicious::Plugin::Mount you can glue them together in this file named mock.pl:
#!/usr/bin/env perl
use v5.42;
use Mojolicious::Lite;
plugin Mount => { '/foo' => './foo.pl' };
plugin Mount => { '/bar' => './bar.pl' };
app->start;
And check the routes:
user@host$ ./mock.pl routes
/foo * foo
/bar * bar
That indicates that all requests starting with /foo will be delivered to the app foo and all requests starting with /bar will be delivered to the app bar.
You could prefix both with / meaning that the original routes will remain root-based.
Let's start the new app:
user@host$ ./mock.pl daemon
[2025-12-07 15:57:34.24246] [1437] [trace] Your secret passphrase needs to be changed (see FAQ for more)
[2025-12-07 15:57:34.24515] [1437] [info] Listening at "http://*:3000"
Web application available at http://127.0.0.1:3000
And make some requests:
user@host$ curl http://localhost:3000/foo/baz
baz
user@host$ curl http://localhost:3000/bar/lala
lala
Note the app logs:
[2025-12-07 15:59:25.05863] [1437] [trace] [PJ0Qy_rY7rEA] GET "/foo/baz"
[2025-12-07 15:59:25.05920] [1437] [trace] [PJ0Qy_rY7rEA] Routing to application "Mojolicious::Lite"
[2025-12-07 15:59:25.06005] [1437] [trace] [PJ0Qy_rY7rEA] GET "/foo/baz"
[2025-12-07 15:59:25.06020] [1437] [trace] [PJ0Qy_rY7rEA] Routing to a callback
[2025-12-07 15:59:25.06046] [1437] [trace] [PJ0Qy_rY7rEA] 200 OK (0.000383s, 2610.966/s)
[2025-12-07 15:59:38.70389] [1437] [trace] [1gOGBF3hsoml] GET "/bar/lala"
[2025-12-07 15:59:38.70418] [1437] [trace] [1gOGBF3hsoml] Routing to application "Mojolicious::Lite"
[2025-12-07 15:59:38.70511] [1437] [trace] [1gOGBF3hsoml] GET "/bar/lala"
[2025-12-07 15:59:38.70529] [1437] [trace] [1gOGBF3hsoml] Routing to a callback
[2025-12-07 15:59:38.70548] [1437] [trace] [1gOGBF3hsoml] 200 OK (0.000363s, 2754.821/s)
That means the request /foo/baz was delivered to the application foo.pl and the request /bar/lala was delivered to the application bar.pl.
You can use that to set up multiple test scenarios by "mounting" different mock apps, like "successful foo with failed bar" or "failed foo with successful bar".
Testing with mocked applications
There are some gotchas when testing with mocked mojo apps. Since Mojo::IOLoop is a singleton, both main app and mocked apps will share the same event loop. That means all blocking calls will make tests hang forever.
So in order to be able to test with mocked apps, your application should:
be able to overwrite the external URLs to point to mocked apps.
have all external calls async.
Here is an example which uses the foo.pl and bar.pl mock apps described above:
Consider the main application below which we will save as app.pl:
#!/usr/bin/env perl
use v5.42;
use Mojolicious::Lite -signatures;
get '/baz-lala' => sub ($c) {
$c->render_later;
# Getting external urls from 'config'
my $foo_url = Mojo::URL->new( $ENV{FOO_SERVER} ) . '/baz';
my $bar_url = Mojo::URL->new( $ENV{BAR_SERVER} ) . '/lala';
# Making async requests
my $foo_p = $c->ua->get_p($foo_url);
my $bar_p = $c->ua->get_p($bar_url);
Mojo::Promise->all( $foo_p, $bar_p )->then(
sub( $foo, $bar ) {
my $res = $foo->[0]->res->text . '-' . $bar->[0]->res->text;
$c->render( text => $res );
}
);
return;
};
app->start;
Here we get the external urls from %ENV and then we make async requests using promises versions of Mojo::UserAgent calls.
Now let's see the test:
#!/usr/bin/env perl
use v5.42;
package MyMock;
use Mojolicious::Lite;
plugin Mount => { '/foo' => './foo.pl' };
plugin Mount => { '/bar' => './bar.pl' };
package MyApp;
use Mojolicious::Lite;
plugin Mount => { '/' => './app.pl' };
package main;
use Test::More;
use Test::Mojo;
my $mock = Test::Mojo->new('MyMock');
# Checking mocks just in case
$mock->get_ok('/foo/baz')->status_is(200)->content_is('baz');
$mock->get_ok('/bar/lala')->status_is(200)->content_is('lala');
# Setting test
$ENV{FOO_SERVER} = $mock->ua->server->url->path('foo');
$ENV{BAR_SERVER} = $mock->ua->server->url->path('bar');
my $t = Test::Mojo->new('MyApp');
# Testing
$t->get_ok('/baz-lala')->status_is(200)->content_is('baz-lala');
done_testing;
Here you can see three packages: MyMock, MyApp and main. They are put together for simplicity but you can have them in separate files.
MyMock is a package to glue together all mocks.
MyApp is a package just to load the main app. If you have a full Mojo app instead you can just load it.
main is the package where we will run the tests. It is not needed if the packages are in different files.
After creating the Test::Mojo we get the urls of mocked apps and set the environment so the main app can get them.
Finally we make tests using Test::Mojo api.
Conclusion
Mojolicious::Plugin::Mount allow us to glue together small mojo apps that could be used to mock different scenarios of external services.
You can mount different mocks depending on the need of each test.
But in order to take advantage of them the application should keep external urls configurable and make external calls async.