Perl Unit Testing: Techniques

G. Wade Johnson

cPanel, Inc.

notes

Unit Testing Techniques

These are the topics I plan to cover today. This won't cover everything you need to know about unit testing in Perl, but it will give you more than the intro talk.

What is Testable Code?

Testable code vs. Code that can be tested.

What do we mean when we say code is testable?

Attributes of Testable Code

All of these attributes make the code easier to test. It's possible to perform multiple tests, changing a single input, and get a defined output.

You may be able to test other code, but it will not be easy.

Reality Check

Global State

Possibly the biggest problem making code hard to test.

Global state may be affected by things outside the scope of your test. This makes repeatable, consistent tests quite difficult.

Global Input

If you code depends on any of these items, it will be harder to test.

Global Output

If you code changes any of these items, it will be harder to test.

Side Effects

Code that affects global state has side effects.

notes

Libraries for Testing

Test::Output Example

use Test::Output;

stdout_is { print "Hello World"; } 'Hello World';
stdout_like { print 'Hello Wade'; } qw/Wade/;

stderr_is { print STDERR 'Hello'; } 'Hello';

Finding Libraries

use lib is your friend

use lib "t/lib";

use FindBin;
use lib "$FindBin::Bin/..";
use lib "$FindBin::Bin/lib";

Making Code Testable

Testing Patterns

The first two are mostly useful for reducing global dependencies and removing coupling between independent systems. The third through fifth are ways to think about generating new tests. The final is magic.

Dependency Injection

Provide global dependency as a parameter.
... Use defaulting for the common case

sub foo {
    my ($parm, $gobj) = (@_);
    $gobj ||= $global_object;
    ...
}

Dependency Injection, Usage

Live code

  foo( $parm );

Test code

  foo( $parm, $fake_global_object );
notes

Dependency Injection, Part Two

Wrap retrieving the global data in a subroutine.
... In the test code, override that subroutine.

package Foo;
sub _get_global_object {
    return $global_object;
}
sub bar {
    my ($parm) = (@_);
    my $gobj = _get_global_object();
    ...
}

Dependency Injection 2, Usage

Live code

  Foo::bar( $parm );

Test code

{ 
    package Foo;
    no warnings 'redefine';
    sub _get_global_object { return $fake_global_object; }
}
  Foo::bar( $parm );
notes

What is Mocking?

notes

Why Mock?

notes

Edge Cases/Boundary Conditions

Bugs lurk in corners and congregate at boundaries.

— Boris Bezier

Most of the inputs of a function are pretty much the same. Boundaries are where behaviour of the function changes. Concentrate in those areas more.

Potential Boundaries

notes

Data-Driven Tests

See example

Error-Handling Testing

notes

Fuzz Testing

Random inputs in the hopes of triggering unusual error conditions.

Often used to attack code.

Testing Anti-Patterns

notes

Test Mode

notes

Saving Data for Test Code

notes

Don't Test

notes

Conclusion

There's a lot more to good testing than just the assertions.

notes

Coverage

How do you know how well you have tested your code?

notes

Levels of Coverage

Statement vs. Branch Coverage

if( get_value() > 0 ) {
    do_it();
}
do_other();

Branch vs. Condition Coverage

if( defined $var && $var > 0 ) {
    do_it();
}
else {
    do_other();
}

Devel::Cover

CPAN module that instruments code to determine what parts of it have been exercised.

Devel::Cover Output

See example

100% Coverage

Don't Trust 100% Coverage

100% coverage is necessary for complete testing, but it may not be sufficient.

Intelligent Testing vs. Code Coverage

Think about what needs to be tested rather than try to hit every line/branch.

Conclusion