Testing a Dancer application

In the previous article, we saw how to develop a plugin. We have written our example as a plugin named Dancer::Plugin::MobileDevice. In this article We'll see how to test it.

As testing a plugin is pretty much the same thing as testing a complete application, you will learn in this article how to use the Dancer::Test module for building a good test suite for a Dancer application.

Why testing?

Writing a plugin for the Dancer ecosystem is a great thing to do; It's a very good way to contribute to the project, but writing the plugin is not enough, you should write tests to validate your plugin before releasing it to the CPAN.

We will now look at how easy it is to do that and we'll write a test suite to make sure everything provided by the plugin works as expected. Remember that tests are still code. Take quality seriously and your software will be better.

The is_mobile_device keyword

Let's start by testing that our helper works as expected, with Dancer::Test this is easy to do:

# t/01-is_mobile_device.t
use strict;
use warnings;
use Test::More import => ['!pass'];

Note that we ask not to import Test::More's pass keyword. It's already exported by Dancer and we don't want our test script to produce a warning.

First of all, we need a basic app that uses our plugin, we'll define it within the test script, inside a lexical block:

{
    use Dancer;
    use Dancer::Plugin::MobileDevice;

    get '/' => sub {
        return is_mobile_device;
    };
}

OK, we have a basic app that just loads the plugin and defines one route handler which only returns the value of is_mobile_device. That's enough, we can now write the tests.

use Dancer::Test;

Dancer::Test provides a complete set of test functions specialized for testing a Dancer application. Here we'll use the function response_content_is which takes a request object (basically an array with a method and a path) and a value, and makes sure the route handler returns a response that is the same as the expected value.

We'll define a bunch of user agent strings we consider "mobile devices" and make sure the flag is set appropriately for them:

my @mobile_devices = qw(Android iPhone PalmOS);

for my $md (@mobile_devices) {
    $ENV{HTTP_USER_AGENT} = $md;
    response_content_is [GET => '/'], 1, "agent $md is a mobile device";
}

And we finally add a non-mobile string:

$ENV{HTTP_USER_AGENT} = 'Mozilla';
response_content_is [GET => '/'], 0, "Mozilla is not a mobile device";

Let's run the test:

$ perl -Ilib t/01-is_mobile_device.t
1..4
ok 1 - agent iPhone is a mobile device
ok 2 - agent Android is a mobile device
ok 3 - agent PalmOS is a mobile device
ok 4 - Mozilla is not a mobile device

Great! We know now for sure that is_mobile_device works, that's a good start!

The default template token

We now want to make sure all of our template calls got the is_mobile_device token. To do that, our test application will now only provide a route handler that calls template. Obviously, for this to work we need... a template to process. Dancer::Test takes care for us to initialize the views directory to t/views, so we can provide our test script with some views without polluting the root directory of our distribution.

So we first create a view:

$ mkdir t/views
$ echo "is_mobile_device: <% is_mobile_device %>" > t/views/index.tt

The view is very basic, it just shows the interpolation of the is_mobile_device token. Let's use it in our test script.

{
    use Dancer;
    use Dancer::Plugin::MobileDevice;

    get '/' => sub {
        template 'index', {}, { layout => undef };
    };
}

Same as previously, we define a route handler that does just what we need. You'll see that we've given some extra options to the template keyword in order to disable the layout. Indeed, our plugin automatically sets a layout for mobile clients and we don't want to see that for the moment.

The second argument given to template is an empty hash which is actually the tokens hash. It should have been populated by our plugin under the hood and we'll make sure of that.

use Dancer::Test;

$ENV{HTTP_USER_AGENT} = 'Android';
response_content_is [GET => '/'],
    "is_mobile_device: 1\n",
    "token is_mobile_device is present and valid for Android";

$ENV{HTTP_USER_AGENT} = 'Mozilla';
response_content_is [GET => '/'],
    "is_mobile_device: 0\n",
    "token is_mobile_device is present and valid for Mozilla";

Let's run the test to make sure everything is fine:

$ perl -Ilib t/02-tokens.t
1..2
ok 1 - token is_mobile_device is present and valid for Android
ok 2 - token is_mobile_device is present and valid for Mozilla

Cool! We now have one more thing to test: making sure the layout changes appropriately, the job of our final test script.

The dynamic layout

In this final test we want to be sure the layout is changed to 'mobile' whenever a mobile device is served, and that the original layout is reset afterwards (whether it was defined or not).

To do that we'll use the same technique as before, but this time with the layouts:

$ mkdir t/views/layouts
$ echo -e "mobile:\n<% content %>" > t/views/layout/mobile.tt
$ echo -e "main:\n<% content %>" > t/views/layout/main.tt

(or use your favourite editor to create our layout files)

We now have our layouts waiting to be used in the t/views/layouts directory, let's use it.

{
    use Dancer;
    use Dancer::Plugin::MobileDevice;

    get '/' => sub {
        template 'index';
    };
}

Just a basic route handler, like before, but this time if a layout is set, we'll use it.

First, we want to test the behaviour of the app when no layout is set:

use Dancer::Test;

$ENV{HTTP_USER_AGENT} = 'Android';
response_content_like [GET => '/'],
    qr{mobile\nis_mobile_device: 1}ms,
    "mobile layout is set for mobile agents";

$ENV{HTTP_USER_AGENT} = 'Mozilla';
response_content_is [GET => '/'],
    "is_mobile_device: 0\n",
    "no layout for non-mobile agents";

And then, when a layout is manually set by the user:

set layout => 'main';

$ENV{HTTP_USER_AGENT} = 'Android';
response_content_like [GET => '/'],
    qr{mobile\nis_mobile_device: 1}ms,
    "mobile layout is set for mobile agents";

$ENV{HTTP_USER_AGENT} = 'Mozilla';
response_content_like [GET => '/'],
    qr{main\nis_mobile_device: 0}ms,
    "main layout for non-mobile agents";

Let's see if everything works:

$ perl -Ilib t/03-layouts.t
ok 1 - mobile layout is set for mobile agents
ok 2 - no layout for non-mobile agents
ok 3 - mobile layout is set for mobile agents
ok 4 - main layout for non-mobile agents

Great, everything works as expected!

Conclusion

Our plugin is now covered by the test suite we've written, and we can publish it to the CPAN. Taking a step further, we could run coverage test with Devel::Cover in order to make sure our test suite goes through all the possible paths but that's beyond the scope of this article.

Author

This article has been written by Alexis Sukrieh.

Copyright

Copyright (C) 2010 by Alexis Sukrieh <sukria@sukria.net>