Приглашаем посетить
Религия (religion.niv.ru)

Section 8.7.  Unit Tests

Previous
Table of Contents
Next

8.7. Unit Tests

As we mentioned in the introduction, the rise of movements like Extreme Programming[*] has led to both a revolution and a resurgence of interest in testing methodologies.

[*] Extreme Programming Explained, by Kent Beck (Addison-Wesley), is the canonical work on the subject.

One particular feature of the XP approach is unit testing, an old practice recently brought back to the limelight; the idea that one should test individual components of a program or module in isolation, proving the functional correctness of each part as well as the program as a whole.

Needless to say, Perl programming devotees of XP have produced a wealth of modules to facilitate and encourage unit testing. There are two major XP testing suites, PerlUnit and Test::Class. PerlUnit is a thorough implementation of the standard xUnit suite, and will contain many concepts immediately familiar to XP devotees. However, it's also insanely complete, containing nearly 30 subclasses and related modules. We'll look here at Test::Class, which can be thought of as unit testing in the Perl idiom. We'll also be examining modules to help with the nuts and bolts of unit testing in areas where it may seem difficult.

8.7.1. Test::Class

The Test::Class module can be viewed in two waysfirst, as a module for testing class-based modules and, second, as a base class for classes whose methods are tests.

Suppose we have the following very simple class, and we want to write a test plan for it:

    package Person;
    sub new {
        my $self = shift;
        my $name = shift;
        return undef unless $name;
        return bless {
           name => $name
        }, $self;
    }

    sub name {
        my $self = shift;

        $self->{name} = shift if @_;
        return $self->{name};
    }

We'll start by writing a test class, which we'll call Person::Test. This inherits from Test::Class like so:

    package Person::Test;
    use base 'Test::Class';

    use Test::More tests => 1;
    use Person;

Tests inside our test class are, predictably, specified in the form of methods. With one slight special featuretest methods are given the :Test attribute. So, for instance, we could test the new method:

    sub constructor :Test {
        my $self = shift;
        my $test_person = Person->new("Larry Wall");
        isa_ok($test_person, "Person");
    }

Notice that the job of emitting the usual Perl ok and not ok messages has not gone awayto do this, we use the Test::More module and make use of its functions inside of our test methods.

Although it may seem initially attractive to name your test methods the same as the methods you're testing, you'll find that you may well want to carry out several tests using and abusing each method. There are two ways to do this. First, you can specify that a particular method contains a number of tests by passing a parameter to the :Test attribute:

    sub name :Test(2) {
        my $self = shift;
        my $test_person = Person->new("Larry Wall");
        my $test_name = $test_person->name();
        is($test_name, "Larry Wall");

        my $test_name2 = $test_person->name("Llaw Yrral");
        is($test_name2, "Llaw Yrral");
    }

Or you could split each test into a separate methodin our Person example, we could have name_with_args and name_no_args or get_name and set_name. In most cases, you'll want to use a mixture of these two approaches.

Section 8.7.  Unit Tests

Never name a test method new. Because your test class inherits from Test::Class, this will override Test::Class's new method causing it to run only one test.

It's fine to define your own test class constructor named new, but make sure it includes the necessary behavior from Test::Class's new or calls SUPER::new.


Once you define all the tests that you want to run, you can then tell Perl to run the tests, using the runtests method inherited from Test::Class:

    _ _PACKAGE_ _->runtests;

With that line in Person::Test, you can run the tests within a test file with just use Person::Test, or on the command line by running perl Person/Test.pm. A more common strategy is to provide a test script that runs all the class tests for a project:

      use Test::Class;
      my @classes;
      Test::Class->runtests(@classes);
      BEGIN {
        my @found = code_to_find_all_classes(  );
        foreach my $class (@found) {
          eval {require $class};
          push @classes if $class->isa('Test::Class');
        }
      }

That's how we define test methods and un the test, but how does Test::Class know which test methods are defined, and in what order does it run the tests?

Well, the truth is quite simplethe Test::Class module goes through the methods defined in the test class, looking for methods marked with the :Test attribute, and it calls them in alphabetical order. (Although, depending on the ordering is generally thought to be a bad thing.)

The problem with this is that sometimes you want an object available all through your testing so you can poke at it using a variety of methods. Our test class, Person::Test, is a real class, and the test methods all get called with a real Person::Test object that can store information just like any other module. We want a fresh Person object in each test to avoid side effects as other test methods alter and test the object repeatedly.

To facilitate this, Test::Class provides another designation for test methodscertain methods can be set to run before each test starts and after each test finishes, to set up and tear down test-specific data. These special methods have special parameters to the :Test attributethose marked as :Test(setup) run before each test, and those marked as :Test(teardown) run after each test. For instance, we could set up our Person:

    sub setup_person :Test(setup) {
        my $self = shift;
        $self->{person} = Person->new("Larry Wall");
    }

and now we can use this object in our test methods:

    sub get_name :Test {
        my $self = shift;
        is ($self->{person}->name, "Larry Wall");
    }
    sub set_name :Test {
        my $self = shift;
        $self->{person}->name("Jon Orwant"); # What a transformation!
        is ($self->{person}->name, "Jon Orwant");

        $self->{person}->name("Larry Wall"); # Better put Larry back.
    }

In other cases, setup may be an expensive process you only want to run once, or side effects may not be an issue because the object is an unchanging resource. Test::Class provides alternatives to setup and teardownmethods marked as :Test(startup) run before the first test method and those marked as :Test(shutdown) run after all the tests have finished. For instance, if our testing requires a database connection, we could set that up in our test object, too:

    sub setup_database :Test(startup) {
        my $self = shift;
        require DBI;
        $self->{dbh} = DBI->connect("dbi:mysql:dbname=test", $user, $pass);
        die "Couldn't make a database connection!" unless $self->{dbh};
    }

    sub destroy_database :Test(shutdown) {
        my $self = shift;
        $self->{dbh}->disconnect;

    }

One useful feature of Test::Class is that it will do its utmost to run the startup and finalization methods, despite what may happen during the tests; if something dies during testing, this will be reported as a failure, and the program will move on to the next test, to assure that the test suite survives until finalization. For this reason, other suggested uses of startup and shutdown methods include creating and destroying temporary files, adding test data into a database (and then removing it or rolling it back again), and so on.

8.7.2. Test::MockObject

One idea behind unit testing is that you want to minimize the amount of code involved in a given test. For instance, let's suppose we're writing some HTML and web handling code that uses an LWP::UserAgent in its machinations. We want to test one subroutine of our program, but to do so would pull in a heap of code from LWP and may even require a call out to a web site and a dependency on particular information there. LWP has its own tests, and we know that it's relatively well behaved. We just want to make sure that our subroutine is well behaved. We also want to avoid unnecessary and unpredictable network access where we can.

Wouldn't it be nice, then, if we could get hold of something that looked, walked, and quacked like an LWP::UserAgent, but was actually completely under our control? This is what Test::MockObject provides: objects that can conform to an external interface, but allow the test developer to control the methods.

Let's first create a new mock object:

    use Test::MockObject;

    my $mock_ua = Test::MockObject->new(  );

This will eventually become the mock LWP::UserAgent that our subroutine uses. In order to be like an LWP::UserAgent, it needs to respond to some methods. We add methods with the mock method:

    $mock_ua->mock('clone',  sub { return $mock_ua    });

Test::MockObject offers a series of alternatives to mocksuch as set_true and set_falsethat are shortcuts for common cases. For example, set_always creates a mock method that always returns a constant value:

    $mock_ua->set_always('_agent', 'libwww/perl-5.65');

After we've built up a set of methods and established what we'd like them to do, we have a mock user agent that can be passed into our subroutine and produce known output to known stimuli.

Section 8.7.  Unit Tests

Be careful that the mock object's interface matches the real object's interface. You could end up with passing tests but failing code if, for example, a mocked method expects an array-reference where the real method expects an array. Integration tests are a good way to protect against this.


This is all very well if we are passing in the object to our routine, but what about the more common case where the routine has to instantiate a new LWP::UserAgent for itself? Test::MockObject can get around thisin addition to faking an individual object, we can use it to fake an entire class.

First, we lie to Perl and tell it that we've already loaded the LWP::UserAgent modulethis stops the interpreter loading the real one and stomping all over our fakery:

    $mock_ua->fake_module("LWP::UserAgent");

Note that this must be done during a BEGIN block or in some other manner before anything else calls use LWP::UserAgent, or else the real module will be loaded.

Now we can use our mock object to create a constructor for the fake LWP::UserAgent class:

    $mock_ua->fake_new("LWP::UserAgent");

After this, any call to LWP::UserAgent->new returns the mock object.

In this way, we can isolate the effects of our tests to a much better-defined area of code and greatly reduce the complexity of what's being tested.

8.7.3. Testing Apache, DBI, and Other Complex Environments

There are many opportunities for us to avoid writing tests, and the more lazy of us tend to take any such opportunity we can find. Unfortunately, most of these opportunities are not justifiedabsolutely any code can be tested in some meaningful way.

For instance, we've seen how we can remove the dependency on a remote web server by using a mock user agent object; but what if we want to test a mod_perl application that uses a local web server? Of course, we could set up a special test Apache instance, something the HTML::Mason test suite does. This is a bit of a pain, however.

Thankfully, there's a much easier solution: we can mock up the interface between our application and Apache, pretending there's a real, live Apache server on the other end of our Apache::Request object. This is a bit more complex than the standard Test::MockObject trick and is certainly not something you'd want to set up in every test you write. The Apache::FakeRequest module gives you access to an object that looks and acts like an Apache::Request, but doesn't require a web server.

In the majority of cases, you can just call your application's handler routine with the fake request object:

    use Apache::FakeRequest;

    my $r = Apache::FakeRequest->new(  );

    myhandler($r);

However, given that the ordinary Apache request is a singleton objectsubsequent calls to Apache->request return the same objectyou may find that lazier programmers do not pass the $r object around, but instead pick it up themselves. To allow testing in the face of this kind of opposition, you will have to override the Apache->request and Apache::Request->new methods, like so:

    use Apache::FakeRequest;

    my $r = Apache::FakeRequest->new(  );
    *Apache::request = sub { $r };
    *Apache::Request::new = sub { $r };

    myhandler($r);

This way, no matter what shenanigans your handler attempts to get a request object, it should always get your fake request.

In some cases, however, you've just got to bite the bullet; if you want to test a database-backed application, you're going to have to set up a database and populate it. How you do this depends on your situation. If you're developing an in-house product, it makes sense to use your real development database and have something like Test::Class's startup and shutdown routines insert and delete the data you need.

If, on the other hand, you're writing a CPAN module and want remote users to be able to test the module, things become more tricky. You can, of course, have them set up a test database and provide your test suite with details of how to access it, but it's difficult to do this while keeping the suite non-interactive: developers using the CPANPLUS module to automatically install modules and their dependencies won't appreciate having to stop and set up a database before going on; neither do software packagers such as those involved in the Debian project need the hassle of setting up a database just for your tests.

In these cases, one decent solution is to use the DBD::CSV or DBD::AnyData modulessimply put your test data into a set of colon-separated files and have your test suite work from that instead of a real RDBMS. If your module requires slightly heavier database access than that, a reasonable solution is DBD::SQLite, a lightweight database server embedded in a C library. This allows you to ship a couple of data files with your tests, giving you pretty much everything you need from a relational database.

    Previous
    Table of Contents
    Next