Beefy Boxes and Bandwidth Generously Provided by pair Networks
No such thing as a small change

Tripwire tests and future-proofing

by dws (Chancellor)
on May 25, 2004 at 06:13 UTC ( #356122=perlmeditation: print w/replies, xml ) Need Help??

Most of the literature on unit testing focuses on testing code. But if you read around the edges you'll find some other uses for unit tests. One use that I've become rather fond of is in writing "tripwire" tests, which serve as an advance warning that some assumption has changed, or that some otherwise unwritten constraint has been violated.

Here's an example from some work that blakem and I did recently. We'd just done a refactoring that involved adding a requirement that each subclass of a factory class provide a class method that returned a unique product code. The question we then asked ourselves was "How can we add a test that would fail if someone violates this requirement in the future?" The answer was to write a tripwire test. The test looked something like this:

# All subclasses return unique product names my @classes = Factory->all_product_classes(); ok( scalar @classes, scalar keys %{{map { $_->product_name() => 1 } @classes}} );

Now, if some later work on the code base involves a new subclass in the factory class hierarchy, and someone forgets to override the product_name method, or overrides it and returns a product name that is used elsewhere in the hierarchy, we have a unit test to catch the problem before it sneaks in to production. The test will fail, someone will look at the test, and they'll be reminded (or informed) of the uniqueness constraint in a way that they might otherwise miss if we'd relied on comments in the superclass or (ugh) external written documentation.

Tripwire tests can be a great way to document constraints and future-proof your code base.

Updated (again) to fix a, uh, syntax error from transcription. Oops. :(

Replies are listed 'Best First'.
Re: Tripwire tests and future-proofing
by EdwardG (Vicar) on May 25, 2004 at 08:15 UTC

    Great suggestion. Reminds me of assertions.

    Update: From the documentation for carp::assert:
    Assertions are the explict expressions of your assumptions about the reality your program is expected to deal with, and a declaration of those which it is not. They are used to prevent your program from blissfully processing garbage inputs (garbage in, garbage out becomes garbage in, error out) and to tell you when you've produced garbage output.


Re: Tripwire tests and future-proofing
by blakem (Monsignor) on May 25, 2004 at 08:38 UTC
    During a code review later in the day, the reviewer asked me "Did you make sure that each class has a unique code?" I got to answer with, "Not only did we verify that they are currently unique, we have a test to ensure that they stay unique!" That's the right answer to a good question.


Re: Tripwire tests and future-proofing
by zby (Vicar) on May 25, 2004 at 08:39 UTC
    I can't understand how works that code example and I think there might be others like me. So could you explain it in some more details? I think this would encrease the learning value of this meditation.

    My hypothesis is that there should be "==" in the place of the "," (assuming that keys in scalar context will return the number of hash elements). But even with that that code would not be trivial.

    Update: Another hypothesis: there should be used the is subroutine (from Test::More) instead the ok subroutine.

      It's simple. ok($$) is a subroutien exported by one of the Test modules. It takes two arguements and compares them for equality. If they're equal, the test passes, if not, the test doesn't pass.

      As to the specifics of his code, theres really two main parts. The inner most part is the keys map { $_->product_name() => } @classes }. This does two things. First the map statement is evaluated and loops through the @classes array, and produces a new list, consisting of every element in @classes interspersed with 1s. So the new list looks like (foo,1,bar,1,baz,1), where foo, bar, and baz are class names in @classes. Then the keys function treats the list that map returns as a hash, and creates a list consisting of all the keys in the new hash. Note that there is some implicit magic going on here, in that a hash can not have two keys with the same name, so if two such keys exists, the first one is ignored and only the last one is returned. This has the effect of returning only a unique list of values.

      Then the list is simply turned in to a number by the scalar operator and compared to the scalar value of @classes. If they are different, then @classes contains multiple items with the same name.

      In bash/linux terms, you might think of it like this: (Warning, pseudo code)
      $lc1 = wc -l my_file.txt; $lc2 = sort my_file.txt | uniq | wc -l; if( $lc1 != $lc2 ) { #test fails } else { #test good! }

      I played around with the example code and, in the presented form, I can't get it to even compile:
      >perl -e"@x=qw/foo bar/; print scalar keys map { $_ => 1 } @x;"
      Type of arg 1 to keys must be hash (not map iterator) at -e line 1, ne +ar "@x;" Execution of -e aborted due to compilation errors.

      This is a trivial "bug" to fix, you simply enclose the map in %{{}} (Which first creates an anonymous hash reference out of a list, then derefences it back in to a hash, which keys will work upon), but in the presented form it doesn't appear to work. This is perl, v5.8.3 built for MSWin32-x86-multi-thread.
        From Test::More:
        ok($this eq $that, $test_name);
        This simply evaluates any expression ($this eq $that is just a simple example) and uses that to determine if the test succeeded or failed. A true expression passes, a false one fails. Very simple.
        So it's not exactly what you describe.

        Additionally a list in scalar context returns it's last value not the number of it's elements. So your explanation of what happens after the keys function is not right.


        $ perl -e '%h = (1, 1, 1, 1); print scalar keys %h, "\n"' 1
        $ perl -e 'print scalar (1, "a"), "\n";' a

        This is a trivial "bug" to fix, you simply enclose the map in %{{}}

        Yeah. That's the problem with late-night transcriptions where proprietary bits need to be discarded. Fixed above.

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://356122]
Approved by davido
Front-paged by grinder
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others pondering the Monastery: (9)
As of 2017-10-22 07:27 GMT
Find Nodes?
    Voting Booth?
    My fridge is mostly full of:

    Results (272 votes). Check out past polls.