[Catalyst] testing catalyst app - need context

David Wright dave-catalyst at dexy.org
Mon Mar 2 22:22:02 GMT 2009


J. Shirley wrote:
> On Mon, Mar 2, 2009 at 5:00 AM, David Wright <dave-catalyst at dexy.org 
> <mailto:dave-catalyst at dexy.org>> wrote:
>
>     Ian Docherty wrote:
>
>         Kate Yoak wrote:
>
>             Hi there,
>
>
>             Here is a newbie question:
>
>             I like to test my functionality in bits and pieces as I
>             write it.  How
>             do I go about getting myself the context object in a test
>             script?
>
>             For example, one of the tests catalyst installs is
>             t/model_App.t where
>             it loads the model.  I'd love to then be able to use the
>             model the same
>             way a controller would:
>
>             my $acc = $c->model('Account')->find(1);
>
>             Instead, I am doing
>             my $model = MyApp::Model::App->new();
>             my $acc = $model->recordset('Account')->find(1);
>
>             In addition to being less than ideal because I am doing
>             something
>             different that I expect real application code to do, it
>             presents
>             configuration problems.  Like, turns out, in order for
>             config to take
>             effect, I have to run __PACKAGE__->setup; after
>             configuring it - which
>             won't be necessary, I think, in a real app.
>
>             Is the practice of unit tests that break up the layers of
>             catalyst
>             frowned upon? Or what should I do to make it work right?
>              
>
>         Best practice is to keep your model separate from Catalyst so
>         that you can (for example) create batch scripts or cron
>         jobs that work on the model without having to load the whole
>         of Catalyst
>
>         Your model then would be in something like MyApp::Storage and
>         accessed by your tests as so...
>
>         my $schema = MyApp::Storage->connect(
>           'DBI:mysql:host=localhost;database=my_database',
>           'username',
>           'password',
>           { 'mysql_enable_utf8' => 1 },
>           {on_connect_do =>[ 'set names utf8' ] }
>         );
>         my $acc = $schema->resultset('Account')->find(1);
>
>         You would be testing your database layer separately from Catalyst.
>
>     Models aren't necessarily database layers.
>
>     I'd suggest that with a sufficiently rich Catalyst application,
>     the 'model' as known by Catalyst could easily become an effective
>     'controller' of a further model. That secondary controller is
>     going to want to load model classes, and Catalyst provides a nice
>     way of controlling instantiation.
>
>     e.g.
>
>     sub handle_payment : Path {
>      my ($self, $c, @args) = @_;
>
>      my $proc = $c->model('PaymentProcessor');
>      my $result = $proc->pay( { currency => $args[0], amount =>
>     $args[1], id => $args[2] } );
>
>      $c->stash->{result}->{message} = $result->message;
>      $c->stash->{result}->{code} = $result->code;
>     }
>
>     # then, within PaymentProcessor
>     sub pay {
>       my ($self, $args) = @_;
>
>       MyApp->model( 'Ledger' )->insert( { # blah } );
>       MyApp->model('Order')->update( { # blah } ) ;
>     }
>
>     This definitely does tightly couple the PaymentProcessor to the
>     application. However, it also allows for better whitebox testing.
>     If your logic is embedded within a Catalyst controller, the only
>     sensible way to test it is to make an HTTP request. If it's in the
>     model, the individual steps can be tested.
>
>     FWIW, the principle here is "Catalyst controllers should do
>     nothing except validate and translate HTTP parameters to 'model'
>     parameters. They shouldn't manipulate model objects, or build
>     complicated logic by combinations of model calls".
>
>     Regards,
>     David
>
>
>
>
> It's good advice to have things standalone and available outside of 
> Catalyst, but your assessment of model dependencies stops short.
> What you want is an independent model that accepts a schema object for 
> recording, and then a very thin adapter class inside of Catalyst.
And the config object, cache backends, the logger? The code above could 
be made more complex:

sub pay {
  my ($self, $args) = @_;
 
  my $order = MyApp->cache('ordercache')->get( $args{id} ) ||
        do {
           my $o = MyApp->model('Order')->find( $args{id} ) ;
           MyApp->cache('ordercache')->set( $o->id, $o, 
MyApp->config->{cache_expiry});
           $o;
   };
   $o->update( { #blah  } );
  MyApp->log->debug ("Got order");
  MyApp->model( 'Ledger' )->insert( { # blah } );
}

As I said, this 'model' class is very controller-like. Something has to 
deal with the interactions between those components, and I contend that 
it shouldn't be a standard Catalyst controller.

Using something like this is the suggested better practice:

package MyApp::Model::PaymentProcessor;
use base 'Catalyst::Model::Adaptor';

__PACKAGE__->config( class => 'MyApp::PaymentProcessor' );

sub prepare_arguments {
  my ($self, $c) = @_;
  return { log => $c->log, expiry => $c->config->{cache_expiry},  cache 
=> $c->cache, schema => $c->model->schema };
}

But this doesn't help much: now, when testing the constructors in my 
'decoupled' model, I have to verify that I am initializing the 
components in precisely the same way that Catalyst does. I'm not 
decoupled at all, I've just made things harder. There's a risk when the 
model is plugged in that  it won't fit, and I'm back at square one with 
working out how to test.

So, if I'm willing to swallow the overhead of 'use MyApp' in a script, 
and I'm pretty sure that my model will never be used outwith the app, 
and if it isn't really any easier to test if the model/controller thingy 
relies on other Catalyst components, why bother decoupling? I certainly 
don't want two independent deployments to deal with, neither of which do 
anything useful in isolation.

David




More information about the Catalyst mailing list