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/