Use Test::More as you experiment

When you’re trying something new, write small programs to test the idea or the new feature. This way, you isolate what you’re doing from the rest of the big application where you might want to use the idea. Some people try to insert the new features directly into the middle of their large programs, but then have problems separating the bugs from the rest of the application with the new thing they want to add.

There are many ways that you can write these small test programs, but you might find the Test::More module handy to verify each step as you figure it out.

Suppose that you want to test the new delete local feature in Perl 5.12. This feature allows you to dynamically remove information from a hash until you reach the end of a scope (see Know what creates a scope). You can temporarily remove a key from a hash, but when you’re done, that key magically re-appears. Since you’ve never used this before, you want to test how it works.

You start with the basic test program. As with any testing, you want to start slowly and verify each step along the way. You can load Test::More and run the trivial pass() test (that always passes), just as a way to start. At the end, you can done_testing() instead of declaring a plan ahead of time. As long as your test program reaches done_testing(), the test harness figures everything that should have run actually did.

use 5.012;
use Test::More;

pass();

done_testing();

When you run that, you shouldn’t be surprised that it passes:

ok 1
1..1

Now that you know that part works, you can add more interesting bits. Since you want to test that a hash has a key, you create a hash and test that the key exists. So far you aren’t testing new features, but merely verifying your starting point:

use 5.012;
use Test::More;

my %hash = ( cat => 'Buster' );
ok( exists $hash{'cat'}, 'The cat key exists' );
is( $hash{'cat'}, 'Buster', 'The cat key has the right value' );

done_testing();

This should work just fine too, and if it doesn’t, you need to adjust your starting position before you test the new feature:

ok 1 - The cat key exists
ok 2 - The cat key has the right value
1..2

Since that works, you can move on the the next part by testing delete local. You should be able to create a scope, delete the key, and see that it’s gone in that scope:

use 5.012;
use Test::More;

my %hash = ( cat => 'Buster' );
ok( exists $hash{'cat'}, 'The cat key exists' );
is( $hash{'cat'}, 'Buster', 'The cat key has the right value' );

SCOPE: {
delete local $hash{'cat'};
ok( ! exists $hash{'cat'}, 'The cat key does not exist in SCOPE' );
}

done_testing();

So far, that works too. The cat key is not in %hash inside the scope you labelled SCOPE. That’s not the whole story, though. After that scope ends, the cat key should reappear. You need to test for that too:

use 5.012;
use Test::More;

my %hash = ( cat => 'Buster' );
ok( exists $hash{'cat'}, 'The cat key exists before SCOPE' );
is( $hash{'cat'}, 'Buster', 'The cat key has the right value before SCOPE' );

SCOPE: {
delete local $hash{'cat'};
ok( ! exists $hash{'cat'}, 'The cat key does not exist in SCOPE' );
}

ok( exists $hash{'cat'}, 'The cat key exists after SCOPE' );
is( $hash{'cat'}, 'Buster', 'The cat key has the right value after SCOPE' );

done_testing();

Now you know that the deletion only mattered in that scope:

ok 1 - The cat key exists before SCOPE
ok 2 - The cat key has the right value before SCOPE
ok 3 - The cat key exists
ok 4 - The cat key exists after SCOPE
ok 5 - The cat key has the right value after SCOPE
1..5

That’s not the whole story though. What other ways does this feature work? What if you call a subroutine within the scope? Does the hash still have the cat key? The local works with a dynamic scope, so it should affect anything outside of the lexical scope too. You can add some subroutine calls. Remember, the lexical %hash is file-scoped in this case, and since you defined it before the subroutines at the end, they bind to the lexical version of %hash:

use 5.012;
use Test::More;

my %hash = ( cat => 'Buster' );
ok( exists $hash{'cat'}, 'The cat key exists before SCOPE' );
is( $hash{'cat'}, 'Buster', 'The cat key has the right value before SCOPE' );

SCOPE: {
delete local $hash{'cat'};
ok( ! exists $hash{'cat'}, 'The cat key does not exist at top' );

some_sub_with_arg( \%hash );
some_sub_without_arg();
}

ok( exists $hash{'cat'}, 'The cat key exists after SCOPE' );
is( $hash{'cat'}, 'Buster', 'The cat key has the right value after SCOPE' );

done_testing();

sub some_sub_with_arg {
	my( $hash ) = shift;
	ok( ! exists $hash->{'cat'}, 
		'The cat key does not exist in some_sub_with_arg' );
	}

sub some_sub_without_arg {
	ok( ! exists $hash{'cat'}, 
		'The cat key does not exist in some_sub_without_arg' );
	}

All of these tests work just fine:

ok 1 - The cat key exists before SCOPE
ok 2 - The cat key has the right value before SCOPE
ok 3 - The cat key does not exist at top
ok 4 - The cat key does not exist in some_sub_with_arg
ok 5 - The cat key does not exist in some_sub_without_arg
ok 6 - The cat key exists after SCOPE
ok 7 - The cat key has the right value after SCOPE
1..7

Are you done yet? Not quite yet. When you think that you understand the feature, you have to try to break it so you know its limitations. What happens if you re-add the cat key when it’s supposed to be locally gone? What is the value after the scope? Try a modified test script:

use 5.012;
use Test::More;

my %hash = ( cat => 'Buster' );
ok( exists $hash{'cat'}, 'The cat key exists before SCOPE' );
is( $hash{'cat'}, 'Buster', 'The cat key has the right value before SCOPE' );

SCOPE: {
delete local $hash{'cat'};
ok( ! exists $hash{'cat'}, 'The cat key does not exist at top' );

$hash{'cat'} = 'Mimi';
ok( exists $hash{'cat'}, 'The cat key exists after re-adding' );
is( $hash{'cat'}, 'Mimi', 'The cat key has the right value after re-adding' );
}

ok( exists $hash{'cat'}, 'The cat key exists after SCOPE' );
is( $hash{'cat'}, 'Buster', 'The cat key has the right value after SCOPE' );

done_testing();

This works too, though:

ok 1 - The cat key exists before SCOPE
ok 2 - The cat key has the right value before SCOPE
ok 3 - The cat key does not exist at top
ok 4 - The cat key exists after re-adding
ok 5 - The cat key has the right value after re-adding
ok 6 - The cat key exists after SCOPE
ok 7 - The cat key has the right value after SCOPE
1..7

Okay, you have to try harder to break it. So far, you’ve used a file-scoped lexical. What if you used re-arranged that to use a hash that you pass in as an argument? Also, what happens if you use a package version of %hash instead of a lexical version?

use 5.012;
use Test::More;

my  %lexical = ( cat => 'Roscoe' );
our %package = ( cat => 'Roscoe' );

foreach my $hash ( \%lexical, \%package ) {
	some_sub_with_arg( $hash );
	}

sub some_sub_with_arg {
	my( $hash ) = shift;
	ok( exists $hash->{'cat'}, 
		'The cat key exists at top of sub' );
	is( $hash->{'cat'}, 'Roscoe',
		'The cat key hash the right value at top of sub' );
	delete local $hash->{'cat'};
	ok( ! exists $hash->{'cat'}, 
		'The cat key does not exist in some_sub_with_arg' );
	}
	
done_testing();

All of these work too:

ok 1 - The cat key exists at top of sub
ok 2 - The cat key hash the right value at top of sub
ok 3 - The cat key does not exist in some_sub_with_arg
ok 4 - The cat key exists at top of sub
ok 5 - The cat key hash the right value at top of sub
ok 6 - The cat key does not exist in some_sub_with_arg
1..6

It’s up to you how far that you want to go, but now that you’ve explored the feature, you have a pretty good idea what the feature does, even beyond your intended use for it.

Things to remember

  • Use Test::More to try new features
  • Verify each step to assure that you’re doing what you think you’re doing
  • Try to break the new feature to find its limitations