J. Shirley wrote:
On Mon, Mar 2, 2009 at 5:00 AM, David Wright <[email protected] <mailto:[email protected]>> 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


_______________________________________________
List: [email protected]
Listinfo: http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/catalyst
Searchable archive: http://www.mail-archive.com/[email protected]/
Dev site: http://dev.catalyst.perl.org/

Reply via email to