Beefy Boxes and Bandwidth Generously Provided by pair Networks
Don't ask to ask, just ask
 
PerlMonks  

Re: How to write testable command line script?

by GrandFather (Saint)
on Nov 20, 2018 at 23:49 UTC ( [id://1226086]=note: print w/replies, xml ) Need Help??


in reply to How to write testable command line script?

It's likely that you are using global variables (as a general thing that is bad) that are not getting initialised before main is called. Try moving all global variables into main - that will break stuff and mean that you have to pass the variables to any sub that needs them, but that is a good thing!

If that doesn't match with your code, show us the code or at least a stripped down version that demonstrates the issue.

Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond

Replies are listed 'Best First'.
Re^2: How to write testable command line script?
by thechartist (Monk) on Nov 21, 2018 at 00:11 UTC
    Here is the most important subroutine that I separated out when I was doing my experiments. Once I get the basics right, I know the tests could be organized more efficiently in a different structure. I was thinking of a hash of arrays. reduce.pl
    #!/usr/bin/env/perl use strict; use warnings; my @answer; #added to suppress warnings, but it still doesn't work. my ($a, $b, $c) = 0; print "0. Value of ARGV is @ARGV; Value of magic array var is @_.\n"; # Take list of arguments from any of the 4 other subroutines # In principle, should accept variable length arguments # and recursively reduce the items in list from right to left. # # Termination: @angle has 1 length. This is pushed onto @answer array +. # Case 1: reduce negative number by adding 60 to it, and # subtracting 1 from number on left. # Case 2: reduce positive number >= 60 by subtracting 60 and # adding 1 to number on left. sub reduce { # print "0. Value of magic array var is @_.\n"; my @angle = @_; # @_ = undef; my ($b, $c) = ($angle[-2], $angle[-1]); # reduce from end. # Warnings when running test script indicate $c in the if statement is + not defined. # But It is defined at the top, and should be defined if the @ARGV var +iable is being passed correctly. if ($c < 0 && scalar(@angle) > 1) { until ($c >= 0 && $c < 60) { $c += 60; $b -= 1; } unshift(@answer, $c); pop(@angle); @angle[-1] = $b; # Debug print statements print "2. b = $b, c = $c\n"; print "2. Angle array is @angle.\n "; print "2. Value of magic array var is @_.\n"; print "2. Values in answer array: @answer.\n"; #### &reduce(@angle); } elsif ($c >= 60 && scalar(@angle) > 1 ) { until ($c < 60 && $c >= 0) { $c -= 60; $b += 1; } unshift(@answer, $c); pop(@angle); @angle[-1] = $b; # Debug print statements print "3. b = $b, c = $c\n"; print "3. Angle array is @angle.\n "; print "3. Value of magic array var is @_.\n"; print "3. Values in answer array: @answer.\n"; #### &reduce(@angle); } elsif ( ($c >= 0 && $c < 60 ) && scalar(@angle) > 1) { unshift(@answer, $c); pop(@angle); &reduce(@angle); } else { unshift(@answer, @angle); print "Reduced answer: @answer \n"; } return $answer; } main( @ARGV ) unless caller(); sub main { &reduce( @ARGV ); }
    Test code:
    #!/usr/bin/env/perl use strict; use warnings; use Test::More 'no_plan'; my $test_path = "C:/Users/Greyhat/PDL_Old/trig/src"; ok( require( "$test_path/reduce.pl" ), 'Load file correctly.' ) or ex +it; my @test_2 = undef; my $answer_2 = 1; my $note_2 = "undef | $answer_2 | 2. Call with no value."; my @test_3 = (180, 59, 58); my @answer_3 = (180, 59, 58); my $note_3 = "@test_3 | @answer_3 | 3. already reduced"; my @test_4 = (179, 0, 60); my @answer_4 = (179, 1, 0); my $note_4 = "@test_4 | @answer_4 | 4. test if single carry works +correctly"; my @test_5 = (179, 59, 60) ; my @answer_5 = (180, 0, 0) ; my $note_5 = "@test_5 | @answer_5 | 5. tests if multiple carry wor +ks correctly"; my @test_6 = (-179, 60, 0); my @answer_6 = (-178, 0, 0); my $note_6 = "@test_6 | @answer_6 | 6. test addition with negative +s"; my @test_7 = (0, 0, -60); my @answer_7 = (-1, 59, 2); my $note_7 = "@test_7 | @answer_7 | 7. test negative borrow works +correctly"; my @test_8 = (90, 360, 360); my @answer_8 = (96, 6, 0); my $note_8 = "@test_8 | @answer_8 | 8. tests if multiple reduce ca +lls work correct"; # Degree Reduction Tests # Similar problems regardless of how I call the function. Neither tes +ting via main() or directly calling reduce() # are effective. ok( reduce() == $answer_2, $note_2 ); ok( reduce( @test_3 ) == @answer_3, $note_3 ); + ok( reduce( @test_4 ) == @answer_4, $note_4 ); + ok( reduce( @test_5 ) == @answer_5, $note_5 ); + ok( reduce( @test_6 ) == @answer_6, $note_6 ); + ok( reduce( @test_7 ) == @answer_7, $note_7 ); ok( reduce( @test_8 ) == @answer_8, $note_8 ); # Degree Subtraction Tests # Degree Multiplication Tests # Degree Division Tests # More Complicated expressions with

      First up: never just add stuff to suppress warnings or errors. Figure out what the cause is and fix the cause!

      In this case the warning was telling you something you needed to know. Lets boil the code down a little to demonstrate the issue:

      #!/usr/bin/env/perl use strict; use warnings; reduce(1); sub reduce { my @array = @_; print $array[-2]; }

      Prints:

      Use of uninitialized value in print at ...\noname1.pl line 10.

      Changing the call to reduce(1, 2); works as expected. The actual problem is you are passing only one argument and using that to populate the array. You then try to access the array expecting two arguments.

      Note that much of your code is fairly old school. In particular don't use & to call functions - it doesn't do what you expect. Also, don't use @array[$idx]. If you want a single element use $array[$id] - $ instead of @.

      Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond

        I appreciate your effort but this is my confusion: The script works correctly at the command line and does not issue any warnings, despite use of the strict and warnings pragmas enabled. It is only when I try to run it using automated test tools do those warnings arise.

        Example: If I simply call reduce with 1 argument at the command line, it will be pushed onto the @answer array and returned.

        This tells me that the instantiation errors are in the way the test code interfaces with the code under test. I don't understand why the arguments aren't being passed to the appropriate subroutine.

        If I attempt to pass a list of scalar arguments in the test code, I get syntax errors when I run:

        perl -c test_file.t

        Also:

      • 1. What should I be doing in the return statements of this particular subroutine so they are easier to test?
      • 2. Should I be testing the subroutine individually, or indirectly accessing the sub through a call to main, as it appears to be done in the Perl Testing: A Developer's Notebook example?

      In line with your code here, this is my take on what one might want to see in an exporting module and its associated .t file. Note that it's not even necessary to go to the trouble of exporting stuff if you're willing to use fully-qualified subroutine names, e.g., TrigCalc::reduce(...);, in the client code — very convenient for smallish, quick-and-dirty modules (update: and still perfectly testable with Test::*). You can see that using a .t file to help specify a program and identify problem behaviors can be very helpful to all concerned.

      The  reduce() function in the .pm file is defined in terms of two non-exported functions. These two functions could easily have been folded into reduce() (it might even have simplified things a bit), but leaving them separate helps document the behavior of the parent function and also provides the opportunity to independently test these two component behaviors even though the functions embodying them are not exported.

      TrigCalc.pm:

      TrigCalc.t (note that I've used a different result for the  (0, 0, -60) test case):

      Update 1: Forgot to include the output of the script. Not really necessary, but I usually do so what the heck...
      Output:

      Update 2: Here's a version of  reduce() with all component behaviors folded into the function. It handles incomplete argument lists better IMHO, also handles undef-s more gracefully. Fully tested.

      sub reduce { # reduce from rightmost end of argument list. # left-pad input arguments with 0s to avoid inaccessible items. my ($degs, # degrees $mins, # minutes $secs, # seconds ) = map $_ || 0, (0, 0, 0, @_)[ -3 .. -1 ]; # handles undefs use integer; $secs = $degs * SECS_PER_DEGREE + $mins * SECS_PER_MINUTE + $secs; $degs = $secs / SECS_PER_DEGREE; $secs %= SECS_PER_DEGREE; $mins = $secs / SECS_PER_MINUTE; $secs %= SECS_PER_MINUTE; return $degs, $mins, $secs; }
      (If you have Perl version 5.10+, the || operator in the  $_ || 0 map expression can be changed to the // defined-or operator.)


      Give a man a fish:  <%-{-{-{-<

        Thanks! This is really helpful; I will study it along with the post by Perlancar. There are some things here that I have to think more about. I like some of the solutions here more than what I had previously considered.

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others admiring the Monastery: (8)
As of 2024-03-28 15:06 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found