Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic
 
PerlMonks  

Re: How to write testable command line script?

by perlancar (Hermit)
on Nov 22, 2018 at 01:29 UTC ( [id://1226157]=note: print w/replies, xml ) Need Help??


in reply to How to write testable command line script?

In line with what davido said, I would also suggest separating your "business logic" from your "plumbing code", where the plumbing code is the environment-specific code to get your business logic code to work (in the case of a command-line script, the plumbing code includes code to parse command-line options and arguments, to respond to --help or --version, to output the result, etc).

I'll use my framework Perinci::CmdLine for illustration here:

### file: trig_calc1.pl #!/usr/bin/env perl use strict; use warnings; our %SPEC; $SPEC{trig_calc} = { v => 1.1, args => { degree1 => {schema=>'int*', req=>1, pos=>0}, minute1 => {schema=>'int*', req=>1, pos=>1}, second1 => {schema=>'int*', req=>1, pos=>2}, op => {schema=>'str*', req=>1, pos=>3}, degree2 => {schema=>'int*', req=>1, pos=>4}, minute2 => {schema=>'int*', req=>1, pos=>5}, second2 => {schema=>'int*', req=>1, pos=>6}, }, args_as => 'array', result_naked => 1, }; sub trig_calc { my ($d1, $m1, $s1, $op, $d2, $m2, $s2) = @_; if ($op eq '+') { my ($dr, $mr, $sr) = (0, 0, 0); $sr += $s1 + $s2; if ($sr >= 60) { $mr += int($sr/60); $sr = $sr % 60 } $mr += $m1 + $m2; if ($mr >= 60) { $dr += int($mr/60); $mr = $mr % 60 } $dr += $d1 + $d2; return [$dr, $mr, $sr]; } else { die "Unknown operation '$op'"; } } if ($ENV{HARNESS_ACTIVE}) { require Test::More; Test::More->import; is_deeply(trig_calc(90, 35, 29, '+', 90, 24, 29), [180, 59, 58]); is_deeply(trig_calc(90, 35, 29, '+', 90, 24, 31), [181, 0, 0]); done_testing(); } else { require Perinci::CmdLine::Any; Perinci::CmdLine::Any->new(url => '/main/trig_calc')->run; }

To run the script on the command-line:

% perl trig_calc1.pl 90 35 29 + 90 24 29
180
59
58

The framework happens to handle most of the plumbing code for you, like parsing command-line arguments and outputting the result. You just need to write your business logic code as a regular Perl function, accepting arguments in @_ and returning result. As a bonus, the framework also generates usage if user specifies --help, generates version information (--version), checks arguments, outputs result as JSON (--json), as well as a few other things.

To test the script, use prove which is the standard harness that comes with perl:

% prove trig_calc1.pl
trig_calc1.pl .. ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.04 cusr  0.00 csys =  0.06 CPU)
Result: PASS

Framework-specific information: As an alternative to specifying tests directly with is_deeply() et al, with the framework you can also put the tests as examples in the function metadata. The examples can be tested using the Test::Rinci module. The benefits of speciying the tests as examples include having the examples shown in --help message as well as generated POD documentation.

### file: trig_calc2.pl #!/usr/bin/env perl use strict; use warnings; our %SPEC; $SPEC{trig_calc} = { v => 1.1, args => { degree1 => {schema=>'int*', req=>1, pos=>0}, minute1 => {schema=>'int*', req=>1, pos=>1}, second1 => {schema=>'int*', req=>1, pos=>2}, op => {schema=>'str*', req=>1, pos=>3}, degree2 => {schema=>'int*', req=>1, pos=>4}, minute2 => {schema=>'int*', req=>1, pos=>5}, second2 => {schema=>'int*', req=>1, pos=>6}, }, args_as => 'array', result_naked => 1, examples => [ { argv => [90, 35, 29, '+', 90, 24, 29], result => [180, 59, 58], }, { argv => [90, 35, 29, '+', 90, 24, 31], result => [181, 0, 0], }, ], }; sub trig_calc { my ($d1, $m1, $s1, $op, $d2, $m2, $s2) = @_; if ($op eq '+') { my ($dr, $mr, $sr) = (0, 0, 0); $sr += $s1 + $s2; if ($sr >= 60) { $mr += int($sr/60); $sr = $sr % 60 } $mr += $m1 + $m2; if ($mr >= 60) { $dr += int($mr/60); $mr = $mr % 60 } $dr += $d1 + $d2; return [$dr, $mr, $sr]; } else { die "Unknown operation '$op'"; } } if ($ENV{HARNESS_ACTIVE}) { require Test::More; Test::More->import; require Test::Rinci; Test::Rinci->import; metadata_in_module_ok('main', {load=>0}); done_testing(); } else { use Perinci::CmdLine::Any; Perinci::CmdLine::Any->new(url => '/main/trig_calc')->run; }

As the script grows, it is recommended to split the business logic to its own module(s) under lib/ (e.g. lib/TrigCalc.pm) and the tests into their separate *.t files in the t/ subdirectory (e.g. t/trig_calc.t). Your script itself is now put in script/ (e.g. script/trig_calc):

### file: lib/TrigCalc.pm package TrigCalc; use strict; use warnings; our %SPEC; $SPEC{trig_calc} = { v => 1.1, args => { degree1 => {schema=>'int*', req=>1, pos=>0}, minute1 => {schema=>'int*', req=>1, pos=>1}, second1 => {schema=>'int*', req=>1, pos=>2}, op => {schema=>'str*', req=>1, pos=>3}, degree2 => {schema=>'int*', req=>1, pos=>4}, minute2 => {schema=>'int*', req=>1, pos=>5}, second2 => {schema=>'int*', req=>1, pos=>6}, }, args_as => 'array', result_naked => 1, }; sub trig_calc { my ($d1, $m1, $s1, $op, $d2, $m2, $s2) = @_; if ($op eq '+') { my ($dr, $mr, $sr) = (0, 0, 0); $sr += $s1 + $s2; if ($sr >= 60) { $mr += int($sr/60); $sr = $sr % 60 } $mr += $m1 + $m2; if ($mr >= 60) { $dr += int($mr/60); $mr = $mr % 60 } $dr += $d1 + $d2; return [$dr, $mr, $sr]; } else { die "Unknown operation '$op'"; } } 1;
### file: script/trig_calc #!/usr/bin/env perl use strict; use warnings; use TrigCalc; use Perinci::CmdLine::Any; Perinci::CmdLine::Any->new(url => '/TrigCalc/trig_calc')->run;
### file: t/trig_calc.t #!perl use strict; use warnings; use TrigCalc; is_deeply(TrigCalc::trig_calc(90, 35, 29, '+', 90, 24, 29), [180, 59, +58]); is_deeply(TrigCalc::trig_calc(90, 35, 29, '+', 90, 24, 31), [181, 0, + 0]); done_testing();

This structure is the standard Perl distribution structure and there are tools to help you create the Perl distribution to upload to CPAN, manage dependencies, and so on.

You test the application using: prove -l (which will run all the t/*.t files), run the application using: perl script/trig_calc but after the distribution is installed, you can just run using trig_calc because the script will be installed to your PATH.

Replies are listed 'Best First'.
Re^2: How to write testable command line script?
by thechartist (Monk) on Nov 22, 2018 at 02:57 UTC

    Thanks! I will have to study this example more closely. I had planned on some revisions as I extend the calculator to have more functionality that you had incorporated into your example.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: note [id://1226157]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others musing on the Monastery: (1)
As of 2024-04-19 18:30 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found