views:

63

answers:

2

I'm collecting a bunch of subroutines that are common to a bunch of my scripts into a module. (I should've done this way earlier, but started out with inherited scripts.) I'm modelling my work on the very helpful example here, using Test::More and Module::Build

All of the subroutines that read or write from files all include a line open() or die "errmsg". I'm in the process of writing a test for the module and ran across this problem. One of the subroutines checks whether a path points to something or not, dying on fail. In the inherited scripts, the subroutine looks like this:

sub checkExist {
  my ($type, $path) = @_;
  if ($type eq 'd') {
    if (! -d $path) {
      warn("dir not found: $path\n");
      die $pathNotFound;
    }
  }
  elsif ($type eq 'f') {
    if (! -f $path) {
      warn("file not found: $path\n");
      die $pathNotFound;
    }
    elsif (! -s $path) {
      warn("empty file: $path\n");
      die $emptyFile;
    }
  }
}

now, I'm testing this with the following line:

is(HomeBrew::IO::checkExist('f', $0), '', "can checkExist find file $0 ?");

which works fine unless I pick a path which doesn't doesn't exist, in which case the test script dies, but the test succeeds, producing the following output:

# Looks like your test exited with 2 just after 5.
Dubious, test returned 2 (wstat 512, 0x200)
All 5 subtests passed 

I would prefer if this were a failing test (rather than a dubious pass), but since this is legacy code, I also want this subroutine to halt execution on fail. What to do? Is it stupid to write a test on a function this simple?

I've already written a checkExist2 function that I'll be using in the future that returns undef on success else a non-zero error (so I can write die if checkExist2() elsewhere). Other suggestions that do not maintain the functionality of checkExist are welcome.

+2  A: 

Why not have a helper subroutine in your test module which wraps an eval{} around HomeBrew::IO::checkExist call and checks for a fail via $@?

sub runcheckExist {
   my $res = eval { HomeBrew::IO::checkExist('f', $0) };
   # May want more logic here 
   # for checking $@ for specific error text pattern
   # or $res
   return $@ ? 1 : 0;
}
my $expect_to_die = 1;
is(runcheckExist(), $expect_to_die, "can checkExist find file $0 ?");
DVK
Why not? because eval is evil, and if you ever find yourself needing it, chances are there is a better way which abstracts away the eval and handles all the edge cases.
Ether
@Ether: say what? what makes you say eval is evil? eval is the correct way to test that code is dying when it should
ysth
@ysth: I generally think that invoking a bare `eval` in real code is a code smell, as almost always you want to use some sort of library that wraps it properly - e.g. Try*, or in this case, Test::Exception does the job nicely. It's doubly smelly if one is looking at `$@` explicitly.
Ether
@Ether: So Test::Exception is evil but its callers are not? I don't buy it. Especially a Try* wrapper; eval{} and `$@` checking are perl's native equivalents to other languages try/catch (if with a few perlish gotchas); mandating use of a syntactic sugar level is silly.
ysth
@Ether - I only partially agree. `eval` is not "evil". Matter of fact, seeing how Test::Exception is merely a bunch of sugar around an "eval", eval is the ONLY way to test this I can think of. However, I agree that, as usual in Perl, a proper CPAN solution is generically better than a homebrew one; with two big caveats: (1) Not all CPAN modules are high quality - YMMV. I don't know anything about Test::Exception yet; but seeing how recommendation comes from you it's definitely worth looking into.
DVK
@Ether ... (2) In some situations, the problem - like in the OP's question IMHO - is so simple and straightforward that a homebrew solution is 100% as reliable and simple as a CPAN module. That's not to say that in a generic situation the module should not be your main considered solution :)
DVK
@DVK: incidentally, I'm not arguing against Test::Exception; it's a fine module in widespread use. I'm arguing against the blanket "eval is evil".
ysth
@ysth - Thx for the rec on the module - I will definitely use that considering both your and Ether's word and the fact that the source code looks thought through and well built :)
DVK
@DVK This response taught me a few things about perl.
flies
@DVK: I use 'evil' loosely, in the same way that I say Britney Spears is evil and sour milk is evil. :) I mean to say that using eval explicitly, rather than a library that wraps it well, is generally the wrong thing to do and should always ring alarm bells in the developer's head if he finds himself typing those four characters. (Sometimes it's quite hard to express the right level of tone in the written word!)
Ether
#define evil "something that vaguely disturbes Ether"
DVK
+6  A: 

The proper way to test if code lives, or dies with a proper error, is with Test::Exception. You can wrap this test around other test cases, since it just takes a coderef:

use Test::More;
use Test::Exception;

lives_ok { is(HomeBrew::IO::checkExist('f', $0), '', "can checkExist find file $0 ?") } '...and code does not die';
Ether
Thanks. This is it.
flies
@flies: I would separate them: `my $exist; lives_ok { $exist = HomeBrew::IO::checkExist(...) } 'checkExist did not die'; is($exist, '', "can find file $0");`. That way you always get two tests run.
ysth