Day 17 - Testing Catalyst Controllers

Unit testing and acceptance testing for Catalyst.

Introduction

Any non-trivial project needs solid QA, and most Catalyst projects are not trivial. This article will guide you through some specific ways to test your controllers.

General Perl testing is beyond the scope of this discussion, but you can read about it in Test::Tutorial, Test::Simple, Test::More, and the Perl Testing Notebook, which is highly recommended.

Think outside the box with Test::WWW::Mechanize::Catalyst

Test::WWW::Mechanize::Catalyst will give you an outside view of your system. You may want to write acceptance tests that cover all the project specs. A set of Mechanize tests can even be the basis of a contract with your client.

For example, T::W::M::C tests will answers questions like: Can I click this link? What does this page contain? If I submit this form, will the content be updated? It is also nice for testing complex user scenarios.

A good way to bootstrap your mechanized tests is passing -mechanize to the controller helper:

 script/myapp_create.pl -mechanize controller Foo

Since we already have the Root controller, we need to create the test file manually:

 t/controller_root.t

 use strict;
 use warnings;
 use Test::More;

 eval "use Test::WWW::Mechanize::Catalyst 'Cheer'";
 plan $@
     ? ( skip_all => 'Test::WWW::Mechanize::Catalyst required' )
     : ( tests => 2 );

 ok( my $mech = Test::WWW::Mechanize::Catalyst->new, 'Created mech object' );

 $mech->get_ok( 'http://localhost/' , 'Requested page');

For a cleaner output I always disable -Debug (in lib/Cheer.pm). You can enable the debug mode when needed by setting the CHEER_DEBUG environment variable:

 CHEER_DEBUG=1 script/cheer_server.pl

get_ok will just examine if you got a 2xx HTTP status. Let's see if the page actually contains what we want:

 $mech->content_like(qr/And best regards for the new year!  There are \d+
 days left until santa comes/, 'Checked content');

Don't forget to update the plan ( tests=>3 ) , and let's run the test:

 perl -Ilib t/controller_testing.t

we should get:

 1..3
 ok 1 - Created mech object
 ok 2 - Requested page
 ok 3 - Checked content

Because we're not using an external http server, we can use any kind of magic to alter our app's environment. A good way to test if the number of days is ok is faking Today() in Cheer::Model::Now. Modify the tests with the following code:

 {no warnings; *Cheer::Model::Now::Today = sub { return 2006,12,5};}

 $mech->get_ok( 'http://localhost/' , 'Requested page');
 $mech->content_like(qr/And best regards for the new year!  There are 20
 days left until santa comes/, 'Checked content');

All tests successful :)

Note we could have done something cleaner using Time::Warp or Test::MockTime if Date::Calc was written in pure Perl.

Ok, now that we have a way to fake time, we can test other scenarios as well. Let's see what happens if it's already Christmas (December 25th) or we just passed Christmas (December 27th):

 {no warnings; *Cheer::Model::Now::Today = sub { return 2006,12,25};}
 $mech->get_ok( 'http://localhost/' , 'Requested page');
 $mech->content_like(qr/It's already Christmas, go check your presents!/, 'Christmas day');

 {no warnings; *Cheer::Model::Now::Today = sub { return 2006,12,27};}
 $mech->get_ok( 'http://localhost/' , 'Requested page');
 $mech->content_like(qr/You just missed it, but there's one next year too!/, 'post Christmas');

Don't forget to update your plan, you have 7 tests now. Let's run it:

 $ perl -Ilib t/controller_root.t
 1..7
 ok 1 - Created mech object
 ok 2 - Requested page
 ok 3 - Checked content
 ok 4 - Requested page
 not ok 5 - Christmas day
 #   Failed test 'Christmas day'
 #   in t/controller_root.t at line 19.
 #          got: "<html><head><title>Happy winter solstice</title></"...
 #       length: 156
 #     doesn't match '(?-xism:It's already Christmas, go check your presents!)'
 ok 6 - Requested page
 not ok 7 - post Christmas
 #   Failed test 'post Christmas'
 #   in t/controller_root.t at line 23.
 #          got: "<html><head><title>Happy winter solstice</title></"...
 #       length: 157
 #     doesn't match '(?-xism:You just missed it, but there's one next year too!)'
 # Looks like you failed 2 tests of 7.

Oops, apparently our app is not ready for these exceptional cases. Let's fix it and pass the tests. Here's the diff for our template:

 --- root/hi_there.tt    (revision 5693)
 +++ root/hi_there.tt    (working copy)
 @@ -1,4 +1,17 @@
  <html><head><title>Happy winter solstice</title></head>
 -<body>And best regards for the new year!  There are [% c.stash.days_till_xmas %]
 -days left until santa comes.  </body>
 +<body>
 +[% IF c.stash.days_till_xmas > 0 %]
 +And best regards for the new year!  There are [% c.stash.days_till_xmas %]
 +days left until santa comes.
 +[% END %]
 +
 +[% IF c.stash.days_till_xmas == 0 %]
 +It's already Christmas, go check your presents!
 +[% END %]
 +
 +[% IF c.stash.days_till_xmas < 0 %]
 +You just missed it, but there's one next year too!
 +[% END %]
 +
 +</body>
  </html>

All tests successful again :)

This concludes our example, but remember we're just scratching the surface of Test::WWW::Mechanize::Catalyst. You can submit forms, authenticate using cookies, and do much more. Please explore this further:

Test::WWW::Mechanize::Catalyst, Test::WWW::Mechanize, WWW::Mechanize, LWP::UserAgent.

Chuck Norris tests his Catalyst controllers with Test::More

While testing your app from outside is very useful to check if your features are implemented as planned, more rigorous testing is needed to ensure the corectness of your code.

When testing controllers you basically want to fake their context, run the action, and check the stash.

Let's do just that. Open a new file, t/controller_root_unit.t:

 use strict;
 use warnings;
 use Test::More tests=>2;
 BEGIN { use_ok 'Cheer::Controller::Root' }

 { no warnings;
 *FakeModel::days_till_xmas = sub { return shift->{days} };
 my $model = bless { days=>5 }, 'FakeModel';
 *FakeContext::model = sub {return $model};
 *FakeContext::stash = sub {return shift->{stash}};
 }
 my $context = bless { stash=> {} } , 'FakeContext';

 Cheer::Controller::Root::index( undef, $context );
 is_deeply ($context->stash, {'template' => 'hi_there.tt','days_till_xmas' => 5}, 'checked stash');

You have there a fake model that always gives 5 days till xmas, and a fake context that returns the fake model.

Later on, after the action is run, you can pick up the results from the fake context's stash.

When unit testing controllers, just make sure you provide all the needed objects in the action's argument list. Make sure every method your code calls has predictable results. And then, just check the stash and you're done.

AUTHOR

Bogdan Lucaciu bogdan@sns.ro