Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl Monk, Perl Meditation
 
PerlMonks  

Unit testing OS rich code

by Voronich (Hermit)
on Oct 12, 2011 at 15:21 UTC ( #931036=perlquestion: print w/ replies, xml ) Need Help??
Voronich has asked for the wisdom of the Perl Monks concerning the following question:

So I've got some code. It's not fancy code, but it should be under test and isn't. I'm usually a pretty strong TDD guy but when writing code that has lots of external interactions, in this case simple filesystem manipulations, it gets sufficiently sticky that I bail out and "just write the damn thing."

The algorithm is:

  • Make sure each of five files exist.
  • Rename them all to something oddly specific to each name.
  • Create an empty ".FLG" version of the renamed file (which will be used
  • as a sentinel to indicate the corresponding desination file exists.)

Here's the approximate look of it. (Note that in the live version the source and destination filenames are sufficiently less patterned that abstracting them further would just be annoying and not really grant me anything.)

# Version Zero sub post_process { my ($path,$yymmdd) = @_; my %rename_table = ("foo_data_a.results" => "bar_abc_foo_data_$yymmdd", ("foo_data_b.results" => "bar_bcd_foo_data_$yymmdd", ("foo_data_c.results" => "bar_cde_foo_data_$yymmdd", ("foo_data_d.results" => "bar_def_foo_data_$yymmdd", ("foo_data_e.results" => "bar_efg_foo_data_$yymmdd", # Don't do anything if any files are missing. for my $src_file (keys %rename_table) { if (!-f "$path/$src_file") { die "$src_file missing. Go blame someone.\n"; } } for my $src_file (keys %rename_table) { # Rename the file my $to_base = "$path/$rename_table{$src_file}"; rename "$path/$src_file","$to_base.results"; `touch $to_base.FLG`; } }
So, ok. No tests. So my "while 1: run tests, wait 5 seconds" test script yacks all over the place. Well, the first fix (for runing in a while(1) test loop) is easy...
# Version One sub post_process { my ($path,$yymmdd) = @_; my %rename_table = ("foo_data_a.results" => "bar_abc_foo_data_$yymmdd", ("foo_data_b.results" => "bar_bcd_foo_data_$yymmdd", ("foo_data_c.results" => "bar_cde_foo_data_$yymmdd", ("foo_data_d.results" => "bar_def_foo_data_$yymmdd", ("foo_data_e.results" => "bar_efg_foo_data_$yymmdd", # Don't do anything if any files are missing. for my $src_file (keys %rename_table) { if (!file_exists("$path/$src_file")) { die "$src_file missing. Go yell at Jorge.\n"; } } for my $src_file (keys %rename_table) { # Rename the file my $to_base = "$path/$rename_table{$src_file}"; rename_file("$path/$src_file","$to_base.results"); touch_file("$to_base.FLG"); } } sub file_exists { my ($filename) = @_; #return (-f $filename); return 1; } sub rename_file { my ($src,$dst) = @_; # return rename $src,$dst return 1; } sub touch_file { my ($filename) = @_; # return `touch $filename`; return 1; }

So now at least the file system operations are abstracted for testing. Now what I didn't do is add a global $TEST_MODE to the script and then pepper the abstraction functions with things like:

my $TEST_MODE = 1; sub file_exists { my ($filename) = @_; if ($TEST_MODE) { return 1; } return (-f $filename); }

...because while that DOES solve the "oh crap, I forgot to replace the mocks with the real versions in the live code" thing that happens, it also makes your code a godawful mess.

So it looks like I'm heading towards some weird "OSOps" class that I can mock out with a duplicated interface in "MockOSOps".

Now... This is getting complicated fast. I'm going from a twenty-something line function into a couple new modules. I'm pretty much sold on the idea that it's the right way to go, despite the fact that I'll end up with what really amounts to a "catch all module" full of stuff I can mock out.

But in the peculiarly touchy-feely language of the Agile Programmers,

It smells off somehow.

Or perhaps I just need another shower.

Thoughts?

Me

Comment on Unit testing OS rich code
Select or Download Code
Re: Unit testing OS rich code
by RichardK (Priest) on Oct 12, 2011 at 15:48 UTC

    I think I would create a temporary directory & files and just run your code against that.

    That way you don't have to mock the os functions at all. In your example it looks easy enough - just touch some file names. Of course if you need to do more complex things, maybe reading info from the files, you may have to keep a set of test files that you copy into your temp dir before you begin the tests.

      Eh. I really don't like having filesystem state for tests. I think I'm just avoiding the inevitable here. I've got the same issues coming fast on the horizon for database interactions as well.
      Me
        I really don't like having filesystem state for tests.

        If your code manipulates the filesystem, how will you know if it works unless you test that it manipulates the filesystem?


        Improve your skills with Modern Perl: the free book.

      Using a temporary directory for test files is fine, until your tests need to do things that your user can't do. For example, my File::Find::Rule::Permissions needs to behave differently for root and for ordinary users, and the tests also need to create directories and files owned by all kinds of different users and groups.

      If my tests are running as an ordinary user, I simply can't create all the structures I need. And if running as root, they *shouldn't*. Dicking about with users and group membership just so my tests can run would be a Really Bad Idea. So I mock up a bunch of filesystem functions so I can test as an ordinary user. Then if my tests are running as root *and* there are appropriate user/group relationships already, I then also create a bunch of test files and run the tests for real.

Re: Unit testing OS rich code
by BrowserUk (Pope) on Oct 12, 2011 at 21:00 UTC

    Essentially all you are testing is:

    if( TRUE ) { # comes here } ...

    And:

    if( FALSE ) { ... } else { # comes here }

    It is tests like these that show up testing for the sake of numbers -- in this case, coverage statistics -- as the meaningless time-wasting exercise it so often is.


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.

      The interesting part isn't the if part. It's the expression.

      If that's not worth testing, so be it—but I've fixed enough bugs in file-handling code that I don't assume that relative paths are correct just because they look like constant expressions.


      Improve your skills with Modern Perl: the free book.

        I would agree, if he was actually performing the file test:

        sub file_exists { my ($filename) = @_; #return (-f $filename); return 1; }

        But once he's mocked that up along with rename which can perform differently on different platforms, and touch, which may well not exist on many machines, all that's left is can perl: a) iterate a hash and b) take the correct branch in the if statement.

        Which is a pointless exercise that serves only to achieve a "100% coverage" statistic. S'no wonder development gets ever more expensive while error rates remain constant.


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Unit testing OS rich code
by eyepopslikeamosquito (Canon) on Oct 13, 2011 at 07:44 UTC

    `touch $to_base.FLG`;
    Are you really running the external touch command via backticks without checking for errors? Eeww. IMHO, Perl internal functions should generally be preferred to running external commands; they tend to be faster, more portable, more secure, and more robust with better error diagnostics. To mimic the touch command in Perl is not difficult. If the file already exists, you could simply use:
    my $now = time(); utime($now, $now, $file);
    Or you might employ the CPAN File::Touch module.

    As for testing this sort of stuff, I don't usually mock at all, just have the test setup create a new empty scratch directory, populate it with known content, test against that, and have the test teardown remove the scratch directory. Oh, and do it all in Perl, not running external commands. :)

      I really don't much care about the timestamp. I'm using touch as the quickest easiest way to create an empty file.
      Me
        the quickest easiest way to create an empty file.

        Quickest? Definitely not. Easiest? Your call :)

        cmpthese 1, { a=>q[ qx[ touch junka.$_ ] for 1 .. 1000 ], b=>q[ do{ open my $f, ">junkb.$_" } for 1 .. 1000 ], };; s/iter a b a 8.83 -- -95% b 0.421 1997% --

        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.

        I'm using touch as the quickest easiest way to create an empty file
        Are you seriously arguing that using:
        `touch $file`
        to create an empty file should be preferred to, for example:
        { open(my $fh, '>', $file) or die "error creating empty '$file': $!"; }
        on the grounds that it is "quicker and easier" to write?

        If you are that stretched for time, how on earth do you expect to find time to write your proposed test mock framework?

Reaped: Re: Unit testing OS rich code
by NodeReaper (Curate) on Oct 13, 2011 at 12:48 UTC
Re: Unit testing OS rich code
by pvaldes (Chaplain) on Oct 13, 2011 at 15:02 UTC
    for all keys if a file is missing die for all keys rename
    ....
    for all keys unless one of the files is missing rename
Reaped: Re: Unit testing OS rich code
by NodeReaper (Curate) on Oct 14, 2011 at 12:38 UTC
Reaped: Re: Unit testing OS rich code
by NodeReaper (Curate) on Oct 16, 2011 at 12:46 UTC
Reaped: Re: Unit testing OS rich code
by NodeReaper (Curate) on Oct 17, 2011 at 13:15 UTC
Reaped: Re: Unit testing OS rich code
by NodeReaper (Curate) on Oct 18, 2011 at 12:48 UTC

Log In?
Username:
Password:

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

How do I use this? | Other CB clients
Other Users?
Others making s'mores by the fire in the courtyard of the Monastery: (12)
As of 2014-07-25 13:59 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    My favorite superfluous repetitious redundant duplicative phrase is:









    Results (172 votes), past polls