Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl-Sensitive Sunglasses
 
PerlMonks  

TDD of non-module code

by davies (Prior)
on Jan 24, 2012 at 11:12 UTC ( [id://949648]=perlquestion: print w/replies, xml ) Need Help??

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

I'm writing some code that's already over 300 lines long. I'm trying to use TDD and have developed some modules that this code will use. For the modules, the TDD procedure seems obvious to me. Module.pm has an "evil twin" called module.t and invoking perl module.t from the command prompt runs the tests. But I don't see a way to do this with code that isn't in a module.

The sources I have read indicate that the same technique can be applied. I can see that technique working if the code to be run from the command line is quite short and all that is needed is to test outputs against a relatively small number of inputs. However, I am manipulating Excel. Therefore, the first thing I do is to is to open an instance of Excel. It is not my plan to expose that sub to any external code if I can possibly avoid it, but I do want to write the code using TDD. By the same token, the code should not leave an orphaned instance of Excel open, so the approach of having an external .t file could not test that the finished article even opens Excel, because by the end of the code, the instance would be closed.

The approaches I have considered are:

  • Put (almost) everything in modules. This makes testing easy, but requires more exposure of subs than I want as well as more linkages between modules (and therefore more scope for blunders by me).
  • Accept that some things just can't be tested. But they can, and the tests I have written so far have saved me from storing up lots of problems for myself.
  • Put the tests in the code to be executed from the command line. This is the approach I am currently using, but I see a problem ahead. When the development is finished, I don't want the tests to run every time. I imagine that this will mean that I will have to put in code to handle options so that code -t will run tests while code filename will do the real work of processing a file.

    Can anyone suggest anything better, please?

    Regards,

    John Davies

  • Replies are listed 'Best First'.
    Re: TDD of non-module code
    by moritz (Cardinal) on Jan 24, 2012 at 12:42 UTC
      Put (almost) everything in modules. This makes testing easy, but requires more exposure of subs than I want

      I don't understand that point at all; maybe your definition of "exposure" is different than mine.

      In Perl, every named subroutine can in principle be called from the outside, independently if it is defined in a script file or a module. (a script can do another script, and then access that second script's subs)

      By convention, subs starting with an underscore are consider "private", so if you want to reduce exposure of a sub inside a module, simply mark it with an underscore.

      as well as more linkages between modules

      I don't see that as a problem either. You can write

      # script.pl # do stuff 1 here

      Or you can write

      # script.pl use Module::That::Does::What::I::Want qw/doit/; doit(@ARGV); # file Module/That/does/What/I/Want.pm package Module::That::Does::What::I::Want; sub doit { # do stuff 1 here }

      And have just one more level of indirection, but an easier time to test all the other subroutines from the module without running doit.

      Of course that approach isn't very different from writing all the code in the script, no code in the mainline, and at the end of the script putting a

      doit(@ARGV) unless caller;

      That way you can also load the script as do 'yourscript.pl' without executing sub doit.

        Your suggestion regarding do looks very interesting. But to go through your points in order:

        My concern about "exposure" (which might not be the best word) is that either I would have to use something like Exporter and end up with sub names in a namespace where I haven't planned for them to be (risking unintended consequences) or I have to use namespace::sub to call them. Maybe it's just prejudice on my part, but this doesn't appeal to me. I can't express my reservations clearly, but I feel uncomfortable doing it.

        As far as adding another level of indirection is concerned, I have no difficulty describing my reservations! :-) I am already passing references around in such a way that leads to code like if (defined $${$${$hashref}{'refTextAry'}}[0]) {. In a separate file, I have over 100 lines of code explaining to myself just why this works. "Just one more level of indirection" fills me with terror. I really wouldn't trust myself to be able to write working tests.

        I am definitely interested in do, though. Unfortunately, the docs seem confusing, at least at first glance.

        do EXPR

        Uses the value of EXPR as a filename and executes the contents of the file as a Perl script.

        do 'stat.pl';

        is just like

        eval `cat stat.pl`;
        isn't a good start for me. The only time I've met cat is as the Unix equivalent of type, which seems to contradict "executes ... a Perl script". Your explanation seems far clearer, but I'll have to do some experimenting to find out what is and isn't possible.

        Thanks & regards,

        John Davies

    Re: TDD of non-module code
    by chrestomanci (Priest) on Jan 24, 2012 at 13:03 UTC
      • Put the tests in the code to be executed from the command line. This is the approach I am currently using, but I see a problem ahead. When the development is finished, I don't want the tests to run every time. I imagine that this will mean that I will have to put in code to handle options so that code -t will run tests while code filename will do the real work of processing a file.

      That sounds like a sensible idea. You could add a --self-test command line switch, and have it on by default during development, and off when the code is complete. You could even put the test code into a separate .pm file (in the same package) so that you don't have to ship the test code in your deliverable.

      An alternative approach would be to create a .t test script that loads and executes your main program, either using system() with a test command line, and then examines the output, or by loading the whole script into a scalar string, and using eval()

      Overall though I think it is wiser in the long term to take moritz's advice and make your script act as both standalone and as a module with doit(@ARGV) unless caller; at the end of all your subroutine definitions

    Re: TDD of non-module code
    by sundialsvc4 (Abbot) on Jan 24, 2012 at 14:02 UTC

      It might help promote a meaningful discussion if you would tell us a little more about your overall testing strategy.   How are you contemplating the process of “testing,” and what (if any ...) Perl test-frameworks are you using?

        This is my first attempt at TDD in Perl. I'm using Test::Simple and nothing more complicated than ok. My approach to coding is to write the test, check that it fails and then write the code to make is pass. I'm trying to write the code from the bottom up, so that the first thing I did was to create the Excel instance. Then I accessed the code modules and made sure that I was getting the sheet names and line counts back. Then, getting sick of closing orphan instances manually, I wrote the code to close the created Excel instance. Then I wrote code to open the output files. And so on.

        I'm not sure if this answers your question "how are you contemplating the process of “testing”", but what I want is a battery of tests that mean that, when I make a change, I can have some reassurance that I haven't made things worse.

        Regards,

        John Davies

          This is my first attempt at TDD in Perl. I'm using Test::Simple and nothing more complicated than ok.

          The Test-Simple distribution includes the Test::More module - since you already have it installed, you might as well use it. For an example of how this will improve your life, consider this test:

          ok(animal() eq 'monkey', 'animal() returned monkey')

          Using the is function from Test::More the same test might be written like this:

          is(animal(), 'monkey', 'check animal() return value')

          The difference is that when your test fails, the diagnostic output will tell you what value was expected and what was actually received - making things much easier to debug.

          Test::More has a number of handy functions like this which will save your time.

    Re: TDD of non-module code
    by Voronich (Hermit) on Jan 25, 2012 at 23:05 UTC

      I'm not sure if it's at all comforting, but you're running in to one of the consistent early hurdles of TDD as it relates to componentized (hush, it is too a word) architecture. TDD nearly demands you expose your code to the tests. I've been an aspiring TDDer for about five years now and still run into this problem like a skier hitting a patch of mud. I've finally convinced myself that the increased exposure pays off more in testability than the protectionism of hiding your deep implementation does in safety.

      THAT said I like the command-line switch idea. What I most frequently do is push everything into a method (including an actual "main" method.) And I keep a "test" method as well. It's not particularly pretty, but then I'm certainly the only one who runs tests on my code.

      At least if you had a testable mode selectable off the command line you'd be able to include your tests in a larger scale test environment.

      Me

        Thanks, all, for the help. I've gone down the command line switch route and have got my code working. But the code has two areas that smell to me, so I come again pleading for help. :-)

        I have tests in three files that are called from a fourth, which is as follows:

        use strict; use warnings; use Test::Harness; runtests(["podext.t"], ["excelpod.bat"], ["ExcelPOD.t"]);

        This works fine, but I don't like the business of having to run the .bat file. However, I can't find any way of putting a command such as excelpod.pl -t with command line parameters into the list of test files to be run without getting an error. The second smell is in excelpod.pl, some of which is:

        use if (1 == scalar @ARGV && "-t" eq $ARGV[0]), "Test::More"; ... if (0 < scalar @ARGV && '-t' eq $ARGV[0]) { #Tests no warnings; eval {"plan tests => 63;"}; use warnings;

        If I write this without all the conditionals, everything works, but running in "live mode" (as opposed to test mode) I get a message saying that no test were run. This might be mistaken for an error message and cause confusion, so I want to suppress it. But writing the "use if" line means that running in live mode generates a compile time error when I give the test plan (and I can find no way of providing that in the "use if" command). Wrapping it in an "eval" command results in a warning of a useless use of a constant in void context, and the only way I can find to avoid that is to suppress warnings.

        As I say, the code is working and the tests are running, but I'd be a happier bunny either if I knew how to avoid the kludges or if I knew that they were unavoidable.

        Regards,

        John Davies

    Re: TDD of non-module code
    by sundialsvc4 (Abbot) on Jan 26, 2012 at 14:48 UTC

      AFAIK, Perl doesn’t have an #ifdef capability, which is unfortunate.   I think that you probably are going to have to put the code into modules, unless you merely intend to be doing “black box” regression-testing, because you do need to be able to isolate the functionality that is under test.   But there are real advantages to modularity that have more value than simply to facilitate unit-testing:   modular code is inherently less coupled because there are only a few well-defined “ways in” and “ways out” for each “thing.”   It is the way that I would design such code anyway.

        I don't know what #ifdef does. I suspect it's something to do with C, but unless someone replies saying how it can be emulated in Perl, it doesn't matter. However, I don't see why modules are the only way to isolate functionality. The code I have written is in quite small subs, meaning that the tests that are in the same file are able to test isolated parts. I have, as I mentioned in my OP, extracted some routines to modules and will continue to do this wherever practical.

        Unfortunately, as far as decoupling goes, there are some things that just have to be sequential and coupled. You can't read from an Excel file until you have opened it and you can't do that until you have created an Excel instance. So, while the subs are individually small with only one entry and exit, executing them before their predecessors is implausible. I'm instinctively keen on separating such code that must be coupled from code that need not, and which is in modules already, so if there are arguments against the command line switch approach, I would like to understand them.

        You may be trying to warn me against Moritz's suggestion. If so, please would you be more specific? I haven't tried working out how to implement that yet and would prefer not to try something impossible.

        Regards,

        John Davies

    Log In?
    Username:
    Password:

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

    How do I use this?Last hourOther CB clients
    Other Users?
    Others browsing the Monastery: (6)
    As of 2024-04-23 11:19 GMT
    Sections?
    Information?
    Find Nodes?
    Leftovers?
      Voting Booth?

      No recent polls found