Michael Schwern - Better programming through testing

Intro

http://www.perlfoundation.org/perl5/index.cgi?testing
http://www.perlfoundation.org/perl5/index.cgi?recommended_modules_for_testing

Test::More 0.88

Fear-based programming: not programming because you fear you might break things

We'll build a date library MyDate.pm with functions like is_leap_year(). Fork http://github.com/schwern/Better-Programming-Through-Testing, git add, commit, push, then send pull requests.

Hands-on

prove reads the TAP (Test Automation Protocol) output and gives a nice summary of test results.

If you don't want to update the number of tests in the plan every time you add new tests (e.g. use Test::More tests => 4), call done_testing() in the END section of the test. The downside is that test progress won't be like "1/4", but "1/?".

Before even bothering with testing, we need to know what the code does. Write POD documentation. A picture is worth a thousand words. In programming, a "picture" is a code usage example:

=head 3 is_leap_year

  my $is_leap_year = MyDate->is_leap_year($year);

Calculates if $year is a leap year.

Returns true if it is, false if not.

=cut

Documentation is a promise. It should not describe much about implementation ("how"), but rather what it does.

Run perldoc MyDate.pm to display the documentation and podchecker to check the syntax of the POD.

Data-driven testing

To avoid having to copy the same test code over and over, and change the description, like below,

ok ( !MyDate->is_leap_year(1989), "1989 is not a leap year");
ok ( MyDate->is_leap_year(1988), "1988 is not a leap year");
ok ( MyDate->is_leap_year(2000), "2000 is a leap year");
ok ( !MyDate->is_leap_year(1900), "1900 is not a leap year");

we can use data-driven testing:

#!/usr/local/bin/perl -w
use strict;
use warnings;

use MyDate;
use Test::More;

use_ok('MyDate');

my %tests = (
    1989 => 0,
    1988 => 1,
    2000 => 1,
    1900 => 0,
);

for my $year (keys %tests) {
    my $want = $tests{$year};

    my $is_leap = MyDate->is_leap_year($year);
    is (!!$is_leap, !!$want"is_leap_year($year)" );
}

done_testing();

Note the use of the "bang-bang" operator to ensure we are comparing boolean values, and not 0 vs. the empty string (for example). Remember, the is_leap_year() function returns true/false, not 0/1 or strings.

Note that we do NOT sort the hash, because in the case of tests, INconsistency is good. We do not want our tests to accidentally depend on order. To this end, prove has a --shuffle switch to randomize the order in which tests are run.

Diagnostics

Normal code:

open my $file, '<', "foo.txt" or die "Can't open: $!";

Testing code:

ok(open my $file, '<', "foo.txt") or die "Can't open: $!";

diag prints to STDERR. note prints to STDOUT.

Git stash

git stash save saves your work without committing: it takes your differences, saves them, then reverts to the last commit.

Testing the Sims

Slides: Generating data with the Sims

Define a sim_person class. It would go in t/lib/MySims.pm.

Random data exercises the code very well. But you can't test random data for equality to some expected date. Three solutions:

  1. You can test it for validity (e.g. no spaces in first_name)
  2. You can create a constructor for sim_person that has random defaults, but also accepts a hash of values to override some of the fields:

    my $person = sim_person(
        # default date of birth
        name => "Weng Weng"
    );
    
  3. Seed the random number generator:

    our $seed = $ENV{TEST_SIMS_SEED} || (time ^ $$)
    srand $seed;
    ...test...
    END {
        my $message = "# TEST_SIMS_SEED: $seed" 
        my $passed = Test::Builder->new->is_passing;
        if ($passed) {
            note $message;
        } else {
            diag $message;
        }
    }
    

A day_of_week function

  • Write the documentation first - this way you'll clarify the requirements before you get bogged down into possibly useless code:
=item day_of_week

  my $dow = day_of_week($year, $month, $day);

Returns the named $dow for the $year, $month and $day.

  print MyDate->day_of_week(2009, 12, 25);  # Friday
=back
=cut

Note how the example in the last POD line indicates clearly that the return value is a string, not the numerical day of the week (0..6).

  • Write the tests before code - this way you won't get lazy and neglect writing tests. It also helps with specifying the interface.

  • When iterating through some numerical space, use a prime number for the step, to avoid falling onto a convenient "rut" or stepping over problematic boundaries.

Testing for exceptions

ok !eval {...}
like $@qr/die message here/;

Better, use Test::Exception:

throws_ok {MyDate->day_of_week(@args)}, qr/^MyDate->day_of_week was given bad arguments/

JavaScript testing

Test::Simple on JSAN - click the "Run test harness" link, it's fun.

Parallel testing

prove -j5

Q&A

"These days, lines of code deleted is a better metric" - Michael Schwern

TODO

DBICx::TestDatabase

My tags:
 
Popular tags: