We have created the following simple Mojolicious controller:
package SampleApp::Pages;
# $Id$
use strict;
use warnings;
our $VERSION = '0.01';
use Mojo::Base 'Mojolicious::Controller';
sub home {
my $self = shift;
$self->render( 'title' => 'Home' );
return;
}
sub contact {
my $self = shift;
$self->render( 'title' => 'Contact' );
return;
}
sub about {
my $self = shift;
$self->render( 'title' => 'About' );
return;
}
1;
The corresponding unit tests look as follows:
package Test::SampleApp::Pages;
# $Id$
use strict;
use warnings;
our $VERSION = '0.01';
use Carp;
use English '-no_match_vars';
use Readonly;
use Test::Mojo;
use Test::Most;
use base 'Test::Class';
Readonly my $SERVER_OK => 200;
sub startup : Tests(startup) {
eval {
require SampleApp;
SampleApp->import;
1;
} or Carp::croak($EVAL_ERROR);
return;
}
sub get_home : Tests(4) {
my $test = shift;
my $mojo = $test->mojo;
$mojo->get_ok('/pages/home')->status_is($SERVER_OK);
$mojo->text_is(
'title',
$test->base_title . ' | Home',
'... and should have the right title'
);
$mojo->content_like(
qr/<body>(?:\s*\S+\s*)+<\/body>/msx,
'... and should have a non-blank body'
);
return;
}
sub get_contact : Tests(3) {
my $test = shift;
my $mojo = $test->mojo;
$mojo->get_ok('/pages/contact')->status_is($SERVER_OK);
$mojo->text_is(
'title',
$test->base_title . ' | Contact',
'... and should have the right title'
);
return;
}
sub get_about : Tests(3) {
my $test = shift;
my $mojo = $test->mojo;
$mojo->get_ok('/pages/about')->status_is($SERVER_OK);
$mojo->text_is(
'title',
$test->base_title . ' | About',
'... and should have the right title'
);
return;
}
sub base_title {
my ( $self, $base_title ) = #_;
if ( defined $base_title ) {
$self->{base_title} = $base_title;
}
return $self->{base_title};
}
sub mojo {
my ( $self, $mojo ) = #_;
if ( defined $mojo ) {
$self->{mojo} = $mojo;
}
return $self->{mojo};
}
sub setup : Tests(setup) {
my $test = shift;
$test->base_title('Mojolicious Sample App');
$test->mojo( Test::Mojo->new( app => 'SampleApp', max_redirects => 1 ) );
return;
}
1;
To us, this is more like functionality testing rather than unit testing
Is there a way to call the home method of the controller and test its output that doesn't require starting up a server instance via Test::Mojo?
To test your controller's wiring, use code such as the following.
We begin t/pages.t with familiar front matter.
use Mojolicious;
use Test::More;
Now create a testing subclass of SampleApp::Pages that records calls to render.
package TestingPages;
use Mojo::Base 'SampleApp::Pages';
has 'render_called';
has 'render_arg';
sub render {
my($self,%arg) = #_;
$self->render_called(1);
$self->render_arg({ %arg });
}
Your question used Test::Class, so continue with that theme.
package Test::SampleApp::Pages;
use base 'Test::Class';
use Test::More;
Note that die with no arguments propagates the most recent exception, so you don't have to write $# explicitly.
sub startup : Test(startup) {
eval { require SampleApp::Pages; SampleApp::Pages->import; 1 } or die;
}
In setup, instantiate the testing subclass, connect it to a Mojolicious instance, and turn off logging.
sub setup : Test(setup) {
my($self) = #_;
my $c = TestingPages->new(app => Mojolicious->new);
$c->app->log->path(undef);
$c->app->log->level('fatal');
$self->{controller} = $c;
}
In the home test, call the controller's home method and inspect the results.
sub home : Tests(2) {
my($self) = #_;
my $c = $self->{controller};
$c->home;
is $c->render_called, 1, "render called";
is $c->render_arg->{title}, "Home", "correct title arg";
}
Finally, run your tests.
package main;
Test::SampleApp::Pages->runtests;
Output:
$ ./sampleapp.pl test
Running tests from '/tmp/sampleapp/t'.
t/pages.t .. ok
All tests successful.
Files=1, Tests=2, 1 wallclock secs ( 0.03 usr 0.02 sys + 0.24 cusr 0.03 csys = 0.32 CPU)
Result: PASS
Now that you see how to do it, the question is whether it's worth all the trouble. Controllers should be simple wiring. Consider whether any complexity in a controller really belongs in a model where testing is much more straightforward.
Related
I want to create a Perl OO module to return a value like DateTime does, but don't know how to it right now. Anyone's help on this will be appreciated.
Below looks like what I wanted:
use DateTime;
use Data::Printer;
my $time = DateTime->now();
print $time . "\n";
print ref $time;
# p $time;
Output:
2022-11-23T13:22:39
DateTime
What I got:
package Com::Mfg::Address;
use strict;
use warnings;
#constructor
sub new {
my ($class) = #_;
my $self = {
_street => shift || "undefined",
_city => shift || "undefined",
_las_state => shift || "undefined",
_zip => shift || "undefined",
};
bless $self, $class;
return $self;
}
#accessor method for street
sub street {
my ( $self, $street ) = #_;
$self->{_street} = $street if defined($street);
return ( $self->{_street} );
}
#accessor method for city
sub city {
my ( $self, $city ) = #_;
$self->{_city} = $city if defined($city);
return ( $self->{_city} );
}
#accessor method for state
sub state {
my ( $self, $state ) = #_;
$self->{_state} = $state if defined($state);
return ( $self->{_state} );
}
#accessor method for zip
sub zip {
my ( $self, $zip ) = #_;
$self->{_zip} = $zip if defined($zip);
return ( $self->{_zip} );
}
sub print {
my ($self) = #_;
printf( "Address:%s\n%s, %s %s\n\n",
$self->street, $self->city, $self->state, $self->zip );
}
1;
# test.pl
#!/usr/bin/perl -w
use strict;
use Data::Printer;
BEGIN {
use FindBin qw($Bin);
use lib "$Bin/../lib";
}
use Com::Mfg::Address;
my $homeAddr = Com::Mfg::Address->new('#101 Road', 'LA', 'CA', '111111');
print $homeAddr;
# $homeAddr->print();
# p $homeAddr;
But this only gives me:
Com::Mfg::Address=HASH(0xb89ad0)
I am curious if print $homeAddr can give me:
something like #101Road-LA-CA-111111 and it really is object like above print $time . "\n";.
I tried to review DateTime source but still have no clue right now.
You're asking how to provide a custom stringification for the object. Use the following in your module:
use overload '""' => \&to_string;
sub to_string {
my $self = shift;
return
join ", ",
$self->street,
$self->city,
$self->state,
$self->zip;
}
This makes
print $homeAddr;
equivalent to
print $homeAddr->to_string();
Given a syntax like
$c->routes->get($path)->to("$controller#$sub");
I would like to know which sub $controller#$sub resolves to on dispatch. Is there a simple method to get the ref of the sub? You can hard-set ->namespaces() so I assume it's not always as simple as $controller::$sub because you could have namespace::$controller::$sub.
I could not find a way to do this using the api, but there is a private method _class() that will give the controller object that contains the sub. Here is an example:
./lib/MyApp/Controller/Foo.pm:
package MyApp::Controller::Foo;
use Mojo::Base 'Mojolicious::Controller';
sub welcome {
my $self = shift;
$self->render(text => 'Hello there.');
}
1;
./myapp.pl:
use strict;
use warnings;
use Mojolicious::Lite;
use lib './lib';
get '/' => sub {
my $c = shift;
$c->render(text => 'Hello World!');
};
my $namespaces = app->routes->namespaces;
push #$namespaces, 'MyApp::Controller';
app->routes->get('/welcome')->to('foo#welcome');
app->hook(
before_dispatch => sub {
my $c = shift;
my $field = { action => "welcome", controller => "foo" };
my $obj = $c->app->routes->_class( $c, $field );
my $method = $field->{action};
my $subref = sub { $obj->$method( #_ ) };
}
);
app->start;
In this example:
$logger->debug({
filter => \&Data::Dumper::Dumper,
value => $ref
});
I can pretty print my references instead of ARRAY(0xFFDFKDJ). But it's too boring to type that long code every time. I just want:
$logger->preprocessor({
filter => \&Data::Dumper::Dumper,
value => $ref
});
$logger->debug( $ref, $ref2 );
$logger->info( $array );
And $ref, $ref2, and $array will be dumped by Data::Dumper.
It there a way to do this?
UPD
With help of your answers I do the patch
Now you just:
log4perl.appender.A1.layout=FallbackLayout
log4perl.appender.A1.layout.chain=PatternLayout
log4perl.appender.A1.layout.chain.ConversionPattern=%m%n
log4perl.appender.A1.warp_message = sub { $#_ = 2 if #_ > 3; \
return #_; }
# OR
log4perl.appender.A1.warp_message = main::warp_my_message
sub warp_my_message {
my( #chunks ) = #_;
use Data::Dump qw/ pp /;
for my $msg ( #chunks ) {
$msg = pp $msg if ref $msg;
}
return #chunks;
}
UPD2
Or you can use this small module
log4perl.appender.SomeAPP.warp_message = Preprocess::Messages::msg_filter
log4perl.appender.SomeAPP.layout = Preprocess::Messages
package Preprocess::Messages;
sub msg_filter {
my #chunks = #_;
for my $msg ( #chunks ) {
$msg = pp $msg if ref $msg;
}
return #chunks;
};
sub render {
my $self = shift;
my $layout = Log::Log4perl::Layout::PatternLayout->new(
'%d %P %p> %c %F:%L %M%n %m{indent=2}%n%n'
);
$_[-1] += 1; # increase level of the caller
return $layout->render( join $Log::Log4perl::JOIN_MSG_ARRAY_CHAR, #{ shift() }, #_ );
}
sub new {
my $class = shift;
$class = ref ($class) || $class;
return bless {}, $class;
}
1;
Yes, of course you can set 'warp_message = 0' and combine msg_filter and render together.
log4perl.appender.SomeAPP.warp_message = 0
log4perl.appender.SomeAPP.layout = Preprocess::Messages
sub render {
my($self, $message, $category, $priority, $caller_level) = #_;
my $layout = Log::Log4perl::Layout::PatternLayout->new(
'%d %P %p> %c %F:%L %M%n %m{indent=2}%n%n'
);
for my $item ( #{ $message } ) {
$item = pp $item if ref $item;
}
$message = join $Log::Log4perl::JOIN_MSG_ARRAY_CHAR, #$message;
return $layout->render( $message, $category, $priority, $caller_level+1 );
}
The easy way: use warp_message
The easiest way to do this is to create a custom appender and set the warp_message parameter so you can get the original references that were passed to the logger:
package DumpAppender;
use strict;
use warnings;
use Data::Dumper;
$Data::Dumper::Indent = 0;
$Data::Dumper::Terse = 1;
sub new {
bless {}, $_[0];
}
sub log {
my($self, %params) = #_;
print ref($_) ? Dumper($_) : $_ for #{ $params{message} };
print "\n";
}
package main;
use strict;
use warnings;
use Log::Log4perl;
Log::Log4perl->init(\q{
log4perl.rootLogger=DEBUG,Dump
log4perl.appender.Dump=DumpAppender
log4perl.appender.Dump.layout=NoopLayout
log4perl.appender.Dump.warp_message=0
});
my $logger = Log::Log4perl->get_logger;
$logger->debug(
'This is a string, but this is a reference: ',
{ foo => 'bar' },
);
Output:
This is a string, but this is a reference: {'foo' => 'bar'}
Unfortunately, if you take this approach, you're stuck writing your own code to handle layouts, open files, etc. I wouldn't take this approach except for very simple projects that only need to print to screen.
A better way: composite appender
A better approach is to write your own composite appender. A composite appender forwards messages on to another appender after manipulating them somehow, e.g. filtering or caching them. With this approach, you can write only the code for dumping the references and let an existing appender do the heavy lifting.
The following shows how to write a composite appender. Some of this is explained in the docs for Log::Log4perl::Appender, but I copied much of it from Mike Schilli's Log::Log4perl::Appender::Limit:
package DumpAppender;
use strict;
use warnings;
our #ISA = qw(Log::Log4perl::Appender);
use Data::Dumper;
$Data::Dumper::Indent = 0;
$Data::Dumper::Terse = 1;
sub new {
my ($class, %options) = #_;
my $self = {
appender => undef,
%options
};
# Pass back the appender to be limited as a dependency to the configuration
# file parser.
push #{ $options{l4p_depends_on} }, $self->{appender};
# Run our post_init method in the configurator after all appenders have been
# defined to make sure the appenders we're connecting to really exist.
push #{ $options{l4p_post_config_subs} }, sub { $self->post_init() };
bless $self, $class;
}
sub log {
my ($self, %params) = #_;
# Adjust call stack so messages are reported with the correct caller and
# file
local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 2;
# Dump all references with Data::Dumper
$_ = ref($_) ? Dumper($_) : $_ for #{ $params{message} };
$self->{app}->SUPER::log(
\%params,
$params{log4p_category},
$params{log4p_level}
);
}
sub post_init {
my ($self) = #_;
if(! exists $self->{appender}) {
die "No appender defined for " . __PACKAGE__;
}
my $appenders = Log::Log4perl->appenders();
my $appender = Log::Log4perl->appenders()->{$self->{appender}};
if(! defined $appender) {
die "Appender $self->{appender} not defined (yet) when " .
__PACKAGE__ . " needed it";
}
$self->{app} = $appender;
}
package main;
use strict;
use warnings;
use Log::Log4perl;
Log::Log4perl->init(\q{
log4perl.rootLogger=DEBUG, Dump
log4perl.appender.Dump=DumpAppender
log4perl.appender.Dump.appender=SCREEN
log4perl.appender.SCREEN=Log::Log4perl::Appender::Screen
log4perl.appender.SCREEN.layout=PatternLayout
log4perl.appender.SCREEN.layout.ConversionPattern=%d %p %m%n
});
my $logger = Log::Log4perl->get_logger;
$logger->debug(
'This is a string, but this is a reference: ',
{ foo => 'bar' },
);
Output:
2015/09/14 13:38:47 DEBUG This is a string, but this is a reference: {'foo' => 'bar'}
Note that you have to take some extra steps if you initialize Log::Log4perl via the API instead of via a file. This is documented in the composite appenders section of the Log::Log4perl::Appender documentation.
I am new to Object oriented programming in perl. So, I have a silly question.
What --
I am writing a script which will do something and write result to stream ( STDOUT or NETWORK ).
How --
[main.pl]
#!/usr/bin/perl
use strict;
require const::StreamTypes;
require output;
my $out = output->new("output");
$out->writeLine("Sample output");
[output.pm]
#!/usr/bin/perl
use strict;
require const::StreamTypes;
package output;
my $OUTSTR;
sub new{
my $class = shift();
my $stream = shift();
if($stream eq const::StreamTypes->STDNET){
}elsif($stream eq const::StreamTypes->STDWEB){
}else{
*OUTSTR = *STDOUT;
}
my $self = {
"_outStream" => $stream,
"_outStreamPtr" => $OUTSTR
};
bless($self, $class);
}
sub writeLine{
my $msg = shift();
print(OUTSTR "$msg\n");
}
return 1;
So, can anyone help me understand what is going wrong here? 'cas program runs without error but with no output.
Thanks!
I changed a couple of things here:
the first parameter of a methd is the invocant (instance or class) itself
indirect file handles are globals!
the autodie module comes in handy, if using open
consider using strict in your modules, too
I would not recommend the use of package global variable ( my $OUTSTR; ), because that's going to be messy with multiple instances, which want to have different streams.
And I definitely got into the habit of using accessors for all attributes. You can use a lightweight system like Class::Accessor or perhaps you are even lucky enough to use Moose our Mouse. Of course there are a couple of other modules also providing accessors in different ways.
package output;
use strict;
use warnings;
use autodie;
use Class::Accessor "moose-like";
has "outStream" => ( is => 'rw' );
sub new{
my ( $class, $stream ) = #_;
my $self = bless( {}, $class );
if ( 0 ) {
# ...
} else {
open( my $outStream, '>&', \*STDOUT );
$self->outStream( $outStream );
}
return $self;
}
sub writeLine{
my ( $self, $msg ) = #_;
print { $self->outStream } "$msg\n";
}
return 1;
Moose would create a constructor for you, but you can insert your parameter processing as easy as follows:
use Moose;
has "outStream" => ( is => 'rw' );
sub BUILDARGS {
my ( $class, $stream ) = #_;
open( my $outStream, '>&', \*STDOUT );
return {
outStream => $outStream,
};
}
$OUTSTR and *OUTSTR are very different things -- you should clear up your misunderstanding about this before you worry about object oriented programming.
That said, you can probably fix this script by getting everything to refer to $OUTSTR:
...
}else{
$OUTSTR = *STDOUT;
}
...
print $OUTSTR "$msg\n";
How about just passing a file handle directly into the object's constructor?
package output;
sub new {
my ($class, $fh) = #_;
bless { file_handle => $fh }, $class;
}
sub writeLine {
my $self = shift;
my $line = shift;
print {$self->{file_handle}} $line;
}
1;
Example usage:
my $output = output->new(\*STDOUT); # write to stdout
my $socket = IO::Socket::INET->new('www.perl.org', PeerPort => 'http(80)', Proto => 'tcp');
my $output = output->new($socket); # write to a socket
Please don't use barenames for file handles. Use lexical file handles.
The following lines assume that there is a hash %type_handlers somewhere that looks something like this:
{ const::StreamTypes->STDNET => \&constructor_for_stdnet_handles
, const::StreamTypes->STDWEB => \&constructor_for_stdweb_handles
}
Then you can replace the bottom of your constructor with:
my $handler = $type_handlers{ $stream };
my $outstr
= $handler ? $handler->()
: do { my $h; open( $h, '>&', \*::STDOUT ) and $h; }
;
return bless( {
_outStream => $stream
, _outStreamPtr => $outstr
}
, $class
);
Then writeLine becomes:
sub writeLine {
my ( $self, $msg ) = #_;
( $self->{_outStreamPtr} || *::STDOUT{IO} )->say( $msg );
}
The method is a little more robust in cases where somebody just blessed themselves into your class.
my $q_and_d = bless {}, 'output';
If you don't want to allow "quick & dirty" instances, and want more precise messages from possible failures, you could do this:
Carp::croak( 'No outstream!' )
unless my $h = Params::Util::_HANDLE( $self->{_outStreamPtr} )
;
I want to export a function which depends on name of class where is exported into. I thought that it should be easy with Sub::Exporter but unfortunately the into key is not passed to generators. I have ended up with those ugly example code:
use strict;
use warnings;
package MyLog;
use Log::Log4perl qw(:easy get_logger);
use Sub::Exporter -setup => {
exports => [
log => \&gen_log,
audit_log => \&gen_log,
],
groups => [ default => [qw(log audit_log)] ],
collectors => ['category'],
installer => \&installer, # tunnel `into` value into generators
};
if ( not Log::Log4perl->initialized() ) {
#easy init if not initialised
Log::Log4perl->easy_init($ERROR);
}
sub gen_log {
my ( $class, $name, $arg, $global ) = #_;
my $category = $arg->{category};
$category = $global->{category}{$name} unless defined $category;
return sub { # return generator
my $into = shift; # class name passed by `installer`
$category = $name eq 'audit_log' ? "audit_log.$into" : $into
if !defined $category; # set default category
# lazy logger
my $logger;
return sub {
$logger or $logger = get_logger($category);
};
};
}
sub installer {
my ( $args, $todo ) = #_;
# each even value is still generator thus generate final function
my $i;
1 & $i++ and $_ = $_->( $args->{into} ) for #$todo;
Sub::Exporter::default_installer(#_);
}
1;
Is there better way how to do it without sacrifice all this rich Sub::Exporter abilities?
For example I would like to use one of those:
use MyLog category => { log => 'foo', audit_log => 'bar' };
use MyLog -default => { -prefix => 'my_' };
use MyLog
audit_log => { -as => 'audit' },
log => { -as => 'my_log', category => 'my.log' };
Edit: Added Sub::Exporter abilities requirement to question.
Edit2: Added usage examples.
You aren't clear how you want to determine the name. If I understand you correctly, this does what you want.
my %sub_for = (
foo => \&foo,
#...
);
sub install_as {
my ($package, $exported_name, $sub) = #_;
no strict 'refs';
*{"$package\::$exported_name"} = $sub;
return;
}
sub get_name_for {
my ($package, $name) = #_;
#... your code here
}
sub import {
my $class = shift;
my $package = caller;
for my $internal_name (#_) {
install_as($package, get_name_for($package, $internal_name), $get_sub_for{$name});
}
return;
}