http://www.perlmonks.org?node_id=931036

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:

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