Beefy Boxes and Bandwidth Generously Provided by pair Networks
Syntactic Confectionery Delight
 
PerlMonks  

Needing help on testing

by Zenzizenzizenzic (Pilgrim)
on Oct 29, 2018 at 20:52 UTC ( [id://1224882]=perlquestion: print w/replies, xml ) Need Help??

Zenzizenzizenzic has asked for the wisdom of the Perl Monks concerning the following question:

Have been working in Perl at my current job for about 3 years. We have currently no in place testing of any sort for the perl code. What I've seen seems to require all the parts to be tested to be in modules. 80% of the code base is in .pl files only. Is there any (easy, hopefully) way to be able to test this code without re-writes?

Replies are listed 'Best First'.
Re: Needing help on testing
by davido (Cardinal) on Oct 30, 2018 at 00:25 UTC

    It doesn't take a tremendous amount of work to convert a script into a modulino. And by so doing, if there are any subroutines within those scripts, they would become testable more in isolation. I wouldn't put the level of work into the category of rewrite, more in the category of refactor, and often it's a pretty simple refactor.

    Consider the following script:

    #!/usr/bin/env perl use strict; use warnings; use IO::Prompt::Tiny qw(prompt); my $user = prompt("What is your name? "); greet($user); exit(0); sub make_hello { return "Hello"; } sub make_subject { return shift; } sub greeting { my $who = shift; return make_hello() . ' ' . make_subject($who) } sub greet { print greeting(shift), "\n"; }

    In its current form you would have to invoke the script, and this can be done from within a test, capturing the output and even providing input. But it's not ideal. Let's convert it to a modulino.

    #!/usr/bin/env perl package MyScript; use strict; use warnings; use IO::Prompt::Tiny qw(prompt); main() if !caller(); exit(0); sub main { my $user = prompt("What is your name? "); greet($user); } sub make_hello { return "Hello"; } sub make_subject { return shift; } sub greeting { my $who = shift; return make_hello() . ' ' . make_subject($who) } sub greet { print greeting(shift), "\n"; } 1;

    Now you can test it more easily:

    use strict; use warnings; use Test::More; require '/path/to/myscript.pl'; can_ok MyScript => qw(greet greeting make_subject make_hello main); is MyScript::make_hello, 'Hello', 'Got Hello'; is MyScript::make_subject('Monk'), 'Monk', 'Got correct subject.'; is MyScript::greeting('Monk'), 'Hello Monk', 'Got a correct greeting.' +; use Capture::Tiny qw(capture_stdout); my $resp = capture_stdout {MyScript::greet('Monk')}; is $resp, "Hello Monk\n", 'Got correct output in STDOUT.'; done_testing();

    The testing code is untested. ;)

    Now you've tested at least minimally each of the main components except for main() itself, and if you wanted to, you could do that too. And all it required was adding the main() unless caller();, wrapping the top level code in a main subroutine, and putting a 1; at the end of the modulino. It really adds about four or five lines of code, and some indenting. You could almost write a script to apply it to all your scripts, and then to do a compile-check to verify they still compile. Keep backups of the original, unaltered scripts, though. ;)


    Dave

      I did some reading on this as well today, will also try some of the other suggestions below. Thanks one and all!
Re: Needing help on testing
by roboticus (Chancellor) on Oct 29, 2018 at 21:56 UTC

    Zenzizenzizenzic:

    You could always try adding a command-line option to invoke a test routine and quit instead of running normally. Suppose, for example, you have the following program:

    #!env perl # # factor.pl <Number> # # Print the list of prime factors for Number # # 20130206 Can't find a copy on my hard drive, so making it again use strict; use warnings; my $num = shift or die "Expected number to factor!"; print "$num: ", join(", ", factors($num)), "\n"; sub factors { my $val=shift; return unless $val; my @primes = (1); if ($val < 0) { unshift @primes, -1; $val = -$val; } while ($val % 2 == 0) { push @primes, 2; $val = $val/2; } my $fac=3; while ($fac*$fac <= $val) { while ($val % $fac == 0) { push @primes, $fac; $val = $val / $fac; } $fac += 2; } push @primes, $val if $val > 1; return @primes; }

    Suppose further that you wanted to make sure that the factors routine was tested. You could create a routine with some tests like this:

    sub do_tests { use Test::More; use Data::Dump 'pp'; my @test_cases = ( [ 0, [] ], [ 1, [1] ], [ 2, [1, 2] ], [ 3, [1, 3] ], [ 4, [1, 2, 2] ], [ -72, [-1, 1, 2, 2, 2, 3, 3] ], ); for my $case (@test_cases) { my @f = factors($case->[0]); # Uncomment for debugging #print "$case->[0], exp: (", join(",",@{$case->[1]}), "), got( +", join(",",@f),")\n"; is_deeply([@f], $case->[1]); } done_testing(); }

    and then change the line that calls it and prints the results to this:

    if ($num eq "-test") { do_tests(); } else { print "$num: ", join(", ", factors($num)), "\n"; }

    So now, I can either use the program normally, or test it:

    $ perl pm_1224882.pl 1234 1234: 1, 2, 617 Roboticus@Waubli ~ $ perl pm_1224882.pl -test ok 1 ok 2 ok 3 ok 4 ok 5 ok 6 1..6

    Unfortunately, a lot of code that was written without tests can be tricky to test. Writing code with tests tends to change your coding style to make your code more testable. So while this trick may help you on your way to starting to get some of your code tested, be aware that retrofitting tests to code you currently have can be a bit tricky.

    I'd suggest trying to add testing where you can, though, as it can make your code better. I don't see any problems with using a trick like this as a starting point.

    In fact, when writing this response, I found a couple cases where my factors routine didn't work. It had an infinite loop if you gave it 0 (it would keep adding '2' to the factors while checking for even numbers), and I didn't make it work for negative numbers at all. The act of me coming up with a few simple test cases made me find and fix those bugs. This little thing has been on my computer for over five years, and I hadn't encountered a problem with it yet. (Not surprising, as I wouldn't call this script to factor 0 or a negative number. But had I moved factors() into one of my libraries and assumed it was fully tested, it would have been found wanting the first time it tried factoring a negative number or 0.

    Another nice thing about test cases is that they can provide some basic documentation on how to use the function, should the comments be inadequate. For example, I should probably return 1, 0 for the factors of 0, but for my purposes, I prefer an empty list, so I'm leaving it that way. If I decide to reuse factors() for another purpose, the result for 0 should be obvious from the test, and I can change the code accordingly if I want 1, 0 in the other use case(s).

    ...roboticus

    When your only tool is a hammer, all problems look like your thumb.

Re: Needing help on testing
by eyepopslikeamosquito (Archbishop) on Oct 30, 2018 at 06:22 UTC
Re: Needing help on testing
by Discipulus (Canon) on Oct 29, 2018 at 21:42 UTC
    Hello Zenzizenzizenzic and welcome to the monastery and to the wonderful world of Perl!

    In my little experience, the short answer is: no, there is no an easy way to test code not contained in modules.

    You can use IPC::Run3 to test your perl script, but you'll find soon that will be not an easy path. If your programs are interactive ones will be not easy at all.

    My humble suggestion is to start familiarizing with tests testing your 20% of codebase that's already in modules, then identify which part of the codebase are critical and try to abstract their functionality in modules and write tests while coding your modules, not after.

    You can also explore the Modulino (see it at mastering perl by brian d foy) tecnique that use caller You'll wrap all the code in a script into a main sub and you'll use main() if not caller(); see also this article at perlmaven.

    See Rescue-legacy-code-with-modulinos too

    Also some link on my bibliotheca can be worth to read.

    L*

    There are no rules, there are no thumbs..
    Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
      ... there is no an easy way to test code not contained in modules.

      Zenzizenzizenzic:   And in general, there's no easy way to test code in a situation in which you've developed a shitload of code and then, and only then, started to think about how you're going to test it all. Let this be an object lesson to all padawans. (I even know of organizations where design and development of the testing environment begins at the same time as — or even well before — application development.)


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

        I even know of organizations where design and development of the testing environmment begins at the same time as — or even well before — application development
        Where do I send my application to?


        holli

        You can lead your users to water, but alas, you cannot drown them.
        "I even know of organizations where design and development of the testing environment begins at the same time as — or even well before — application development."

        This is personally how I approach nearly each piece of software I write (whether it be Perl, Python, C, C# etc), and most of my code has 98%+ coverage (with coverage being covered in dozens if not hundreds of different angles).

        That said, the vast majority of companies I've worked for or contracted for don't take this approach as it's too costly. Businesses need to get product out the door, so proper Test Driven Development (TDD) adds a massive delay to realizing Return on Investment so it's mostly eschewed for develop it now, hope it works, but if we have to fix a bug later, THEN add tests for that one piece.

        It's a sad reality that profits outweigh quality, but that is the world we live in, unfortunately.

        Now, if you know of a specific shop that does use TDD by default, please let me know who :)

        I even know of organizations where design and development of the testing environment begins at the same time as — or even well before — application development.

        Those are the ones that never finished their original project, because they spent all the time debugging their tests that have been written by the same people as the actual project code.

        perl -e 'use MIME::Base64; print decode_base64("4pmsIE5ldmVyIGdvbm5hIGdpdmUgeW91IHVwCiAgTmV2ZXIgZ29ubmEgbGV0IHlvdSBkb3duLi4uIOKZqwo=");'
        We're getting a couple new programers soon, so I'd like to be able to teach them "right" vs. "the way we've always done it".
Re: Needing help on testing
by Your Mother (Archbishop) on Oct 29, 2018 at 21:52 UTC

    Perl testing is almost certainly the most powerful of any programming language. You need to provide a concrete example though if you want concrete answers. If you want to explore the topic at your leisure, this is a treasure trove of testing advice: Perl Testing: A Developer's Notebook: A Developer’s Notebook.

Re: Needing help on testing
by pryrt (Abbot) on Oct 29, 2018 at 21:57 UTC

    For testing any functions in your codebase.pl files, I believe you should be able to create a test script which has use lib '.'; use 'codebase.pl'; -- anything that's automatically run in your code will, I think, be thrown away (unless the return code is not TRUE -- in which case, the use 'codebase.pl' will fail). If there's too much user-interface (prompting for inputs, etc), I don't know how successful the use will be. But assuming it gets past that point, you can then test the functions from codebase.pl in the same way you would test functions from modules. And you can then eventually move those functions into modules.

    Alternatively, you could create a new module newmod.pm (for appropriate package names, and directory hierarchy), and add the line use newmod; to codebase.pl. Then, one by one, move functions out of the codebase.pl into newmod.pm; with each function this_fn() that you move, make sure codebase.pl still runs as it used to (with a quick manual test) to make sure it wasn't using a file-lexical that it shouldn't be, and write up the appropriate t/*.t test suite for the function that's moved into newmod.pm. (Often times, you can make the this_fn.t look very similar to calls from your original codebase.pl). Continue this process until the codebase.pl just contains the script, and all the functions are moved to modules where they can be easily tested.

Re: Needing help on testing
by Anonymous Monk on Oct 30, 2018 at 02:38 UTC
    Perl::Critic doesn't exactly test the code, but its analysis is very useful in revealing all sorts of issues.
Re: Needing help on testing
by Anonymous Monk on Oct 29, 2018 at 21:27 UTC
    what does the code do, what is the i/o? file stuff, database stuff, web stuff, network stuff...? you gotta tell us more
      Code base covers a large area, web pages, log scrapers, maintenance script. So that's why I was looking for more generic assistance.

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others avoiding work at the Monastery: (2)
As of 2024-04-19 21:31 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found