Chapter 8. Unit Testing with Test::Class

If you have experience in other object-oriented languages, you may have used unit testing to develop your test cases and test suites. Object-oriented unit testingframeworks are more popular with programming languages such as C# and Java, while the majority of Perl tests are procedural. This isn’t to say that one style is better than the other—the choice between styles depends on the goal and structure of your software.

Test::Class is a powerful testing library that allows you to design your tests in the xUnit style. Tests using Test::Class are classes, not just simple test files. This is more complicated to start, but it allows you to organize test cases more easily as well as minimize repetitive testing code, especially for heavily object-oriented projects.

This chapter demonstrates how to write unit testing code in Perl with Test::Class to take advantage of its benefits, including fixtures and inheritance.

Writing Test Cases

Consider a Queue object that stores items to access in first-in, first-out order. Queue allows you to enqueue and dequeue items, returning them in insertion order. You can query a Queue for how many items it contains. Sure, it’s simple enough to do this with Perl’s basic data structures, but the complexity of Queue could grow quickly as its uses supersede what a normal array provides.

This lab demonstrates how to test Queue by creating a module that subclasses Test::Class.

How do I do that?

Create a directory Queue/ and save the following as Queue/Test.pm:

    package Queue::Test;
    
    use base 'Test::Class';
    
    use Queue;
    use Test::More;
    
    sub size : Test(4)
    {
        my $q1 = Queue->new();
        isa_ok( $q1, 'Queue' );
        is( $q1->size(), 0, 'an empty queue' );
    
        my $q2 = Queue->new(qw( howdy bonjour ));
        isa_ok( $q2, 'Queue' );
        is( $q2->size(), 2, 'a queue with some elements' );
    }
    
    sub enqueue : Test(2)
    {
        my $queue = Queue->new();
        isa_ok( $queue, 'Queue' );
    
        $queue->enqueue($_) for qw( howdy bonjour );
        is( $queue->size(), 2, 'queue is now larger' );
    }
    
    sub dequeue : Test(6)
    {
        my $queue = Queue->new();
        isa_ok( $queue, 'Queue' );
    
        is( $queue->dequeue, undef, 'empty queue' );
    
        $queue->enqueue($_) for qw( howdy bonjour );
        is( $queue->size(),    2,         'queue is now larger'  );
        is( $queue->dequeue(), 'howdy',   'first item'           );
        is( $queue->dequeue(), 'bonjour', 'second item'          );
        is( $queue->size(),    0,         'queue is now smaller' );
    }
    
    1;

The Queue class is fairly simple as far as Perl objects go. Save it as Queue.pm:

    package Queue;
    
    use strict;
    use warnings;
    
    sub new
    {
        my ($class, @items) = @_;
        bless @items, $class;
    }
    
    sub size
    {
        my ($self) = @_;
        return scalar @$self;
    }
    
    sub enqueue
    {
        my ( $self, $item ) = @_;
        push @$self, $item;
    }
    
    sub dequeue
    {
        my ( $self ) = @_;
        return shift @$self;
    }
    
    1;

Save the test file as queue.t:

    #!perl
    
    use Queue::Test;
    
    Test::Class->runtests();

Finally, run queue.t with prove:

    $ prove queue.t
    queue....#
    # Queue::Test->test_dequeue
    1..12
    ok 1 - The object isa Queue
    ok 2 - empty queue
    ok 3 - queue is now larger
    ok 4 - first item
    ok 5 - second item
    ok 6 - queue is now smaller
    #
    # Queue::Test->test_enqueue
    ok 7 - The object isa Queue
    ok 8 - queue is now larger
    #
    # Queue::Test->test_size
    ok 9 - The object isa Queue
    ok 10 - an empty queue
    ok 11 - The object isa Queue
    ok 12 - a queue with some elements
    ok
    All tests successful.
    Files=1, Tests=12,  1 wallclock secs ( 0.19 cusr +  0.00 csys =  0.19 CPU)

What just happened?

The test file you saved as queue.t has a very simple job: to run all of the test methods defined in the Queue::Test class. Test::Class is smart—it keeps track of any module that subclasses it. All you need to do is use your test modules and call runtests() on Test::Class itself.

You can use any Test::Builder testing module with Test::Class, such as Test::Exception or Test::Deep. Most test classes use at least Test::More’s basic testing functions.

To designate a method as containing tests, add a Test( n ) attribute that declares how many tests the method contains. Test::Class automatically adds them all up and declares a plan for you, so you don’t need to scan through giant test files to count all of your is() and ok() functions. If you don’t know how many tests a method will contain, use the Test(no_plan) attribute.

Note

Subroutine attributes are the things after the subroutine name and before the opening brace. See perldoc attributes to learn more.

If your test methods die or return before the end of the test method, Test::Class will produce fake skipped tests enough times to complete the test count declared in the Test attribute. Dying in a test method produces a test failure, and returning skips the remaining tests in the method. However, if you return when you use Test(no_plan), you won’t have any idea if there are tests after the return statement that should have run!

When you run your tests with verbose mode (either by using the -v option with prove or by setting the TEST_VERBOSE environment variable), Test::Class outputs the name of the test method before it runs any tests for that method. This is a nice way to see where certain tests come from while debugging. Also, if you don’t specify test descriptions in your test functions, Test::Class uses the name of the current test method as the test description.

What about...

Q: Should I use Test in all of my module names?

A: The standard naming convention for unit testing is to suffix the class name you’re testing with Test. The example code in this lab used this convention for clarity, but naming your classes like this isn’t completely necessary.

An alternative naming scheme for the test classes is to name them in the manner of other object-oriented modules. For example, the Queue::Test::Word class inherits from Queue::Test. Opinions vary on which is the best approach, so choose the style that fits your team and project.

Q: What if I distribute this module? Will my test classes install along with my other modules?

A: If your Makefile.PL or Build.PL doesn’t explicitly state what modules it’s going to install, yes. By default, ExtUtils::MakeMaker and Module::Build look in the lib/ directory of the distribution for any modules to install. If you don’t want to install your test classes, see "Using Temporary Databases" in Chapter 6, which describes using a separate build_lib/ directory for the testing-related modules.

Of course, if your project is a framework you expect people to subclass, installing the test modules will allow them to inherit tests as well.

Q: Can I control the order in which the tests run?

A: Test::Class runs all groups of tests in alphabetical order. First, all startup methods run in alphabetical order. Next, the test methods run in alphabetical order. Finally, the shutdown methods run in alphabetical order. For every test method, its setup methods run in alphabetical order. Then the test method itself runs. Finally, its teardown methods run in alphabetical order. (”Creating Test Fixtures,” next, explains setup and teardown methods and fixtures.)

Creating Test Fixtures

Imagine writing tests for your car. If you turn the wheel, do the tires turn left? What about right? If you hit the brakes, do the rear lights light up? Of course, before you can perform any of these tests, you need to open the door, sit in the driver’s seat, put on the seat belt, and start the car. When you’re done, you must stop the car, unbuckle, and disembark. What a pain it would be to perform each step for each individual test—you’d have to get in and start the car three times!

It would be much easier if, before each test, your car arrived fully prepared and then magically transported you to the driver’s seat, buckled you in, and fastened your crash helmet securely. This is exactly what fixtures are: parts of an environment created before tests run and removed after the tests finish.

This lab shows how to create fixtures for your tests using setup and teardown methods, which eliminates duplication and makes your test code more sane.

How do I do that?

Copy the Queue module and queue.t test file from "Writing Test Cases.” However, the test module needs to change slightly. The new Queue::Test needs a new method, setup_queues(), to create a test fixture for the other test methods to use.

Save the following code as Queue/Test.pm:

    package Queue::Test;
    
    use base 'Test::Class';
    
    use Queue;
    use Test::More;
    
    sub setup_queues : Test( setup => 2 )
    {
        my ($self) = @_;
    
        $self->{empty}    = Queue->new();
        $self->{twoitems} = Queue->new(qw( howdy bonjour ));
    
        isa_ok( $self->{$_}, 'Queue' ) for qw( empty twoitems );
    }
    
    sub size : Test(2)
    {
        my ($self) = @_;
        is( $self->{empty}->size(),    0, 'an empty queue'             );
        is( $self->{twoitems}->size(), 2, 'a queue with some elements' );
    }
    
    sub enqueue : Test(1)
    {
        my ($self) = @_;
        $self->{twoitems}->enqueue($_) for qw( ciao yo );
        is( $self->{twoitems}->size(), 4, 'queue is now larger' );
    }
    
    sub dequeue : Test(3)
    {
        my ($self) = @_;
    
        is( $self->{empty}->dequeue(),    undef,     'empty queue' );
    
        is( $self->{twoitems}->dequeue(), 'howdy',   'first item'  );
        is( $self->{twoitems}->dequeue(), 'bonjour', 'second item' );
    }
    
    1;

Run queue.t verbosely with prove:

    $ prove -v queue.t
    queue....#
    # Queue::Test->dequeue
    1..12
    ok 1 - The object isa Queue
    ok 2 - empty queue
    ok 3 - queue is now larger
    ok 4 - first item
    ok 5 - second item
    ok 6 - queue is now smaller
    #
    # Queue::Test->enqueue
    ok 7 - The object isa Queue
    ok 8 - queue is now larger
    #
    # Queue::Test->size
    ok 9 - The object isa Queue
    ok 10 - an empty queue
    ok 11 - The object isa Queue
    ok 12 - a queue with some elements
    ok
    All tests successful.
    Files=1, Tests=12,  0 wallclock secs ( 0.16 cusr +  0.03 csys =  0.19 CPU)

What just happened?

Every test method receives a hash reference as its first argument. This is the test object, and it exists to pass data from the fixtures to the tests. Feel free to add whatever you want to it.

Notice the output of prove -v? There are a total of six isa checks, yet setup_queues() is the only method that calls isa_ok(), and it does so only twice. What happened? setup_queues() has the attribute Test(setup=> 2).

Note

Test(setup)is the same as Test(setup =>0). The same goes for the teardown, startup, and shutdown attributes. It never hurts to be verbose, though.

The setup_queues() method prepares and checks the type of two Queue objects that all of the test methods use. Test::Class calls setup_queue() before each test method, so it runs three times in this test file. Each test method receives two fresh Queue objects in the test object. This simplifies the testing code by eliminating duplicate code, making it easier to add new tests.

What about...

Q: What if I need to clean up the fixture after each test?

A: Use a teardown method by creating a new method with the attribute Test(teardown => n ). Teardown methods run after each test method.

Q: Is it possible to have setup and teardown methods for the entire class?

A: Sure! Test::Class calls these startup and shutdown methods. Declare them with the attributes Test(startup => n) and Test(shutdown => n), respectively. Each startup and shutdown method runs only once per test file. It receives the test object as the first argument, just like the other test methods.

Because startup methods run only once at the beginning of the test, they do not have the chance to reinitialize whatever they store in the test object as setup methods do.

Inheriting Tests

Your boss thinks highly of your new, shiny Queue module. “Great,” she says, “but we need a subclass that will enqueue only single, unhyphenated words.” Before you became a confident tester, this might have worried you. It’s not scary anymore, though.[1] Thanks to Test::Class, there’s not much more to do.

This lab explains how to write tests for subclasses when you already have Test::Class tests for their parents.

How do I do that?

A subclass inherits from a parent class, so why not have tests inherit from a parent test? Except for the enqueue() method, the features of the two classes are the same. Because the tests for Queue enqueue only words, you can reuse the test methods declared in Queue::Test.

Create the directory Queue/Word/, and save the following as Queue/Word/Test.pm:

    package Queue::Word::Test;
    
    use base 'Queue::Test';
    
    use Queue::Word;
    use Test::More;
    use Test::Exception;
    
    sub setup_queues : Test( setup => 2 )
    {
        my ($self) = @_;
    
        $self->{empty}    = Queue::Word->new();
        $self->{twoitems} = Queue::Word->new(qw( howdy bonjour ));
    
        isa_ok( $self->{$_}, 'Queue::Word' ) for qw( empty twoitems );
    }
    
    sub check_only_words : Test(5)
    {
        my ($self) = @_;
    
        lives_ok { $self->{empty}->enqueue('wassup') } "can enqueue words";
        lives_ok { $self->{empty}->enqueue('HeLlO') } "case doesn't matter";
        dies_ok  { $self->{empty}->enqueue(1981) } "can't enqueue integers";
        dies_ok  { $self->{empty}->enqueue(10.9) } "can't enqueue decimal";
        dies_ok  { $self->{empty}->enqueue('Transzorp Diode') }
            "can't enqueue names of cyborgs";
    }
    
    1;

Next, create the Queue::Word module that extends Queue. Save the following code as Queue/Word.pm:

    package Queue::Word;
    
    use strict;
    use warnings;
    
    use base 'Queue';
    
    sub enqueue
    {
        my ( $self, $item ) = @_;
    
        die "can only enqueue words, not '$item'"
            unless $item =~ m/ ^ [A-Z]+ $ /ix;
    
        push @$self, $item;
    }
    
    1;

Now create a test file, queue_word.t, so that it runs the tests for both classes. Save the following code as queue_word.t:

    #!perl
    
    use Queue::Test;
    use Queue::Word::Test;
    
    Test::Class->runtests();

Run it with prove:

    $ prove queue_word.t
    queue_word....ok
    All tests successful.
    Files=1, Tests=31,  1 wallclock secs ( 0.07 cusr +  0.00 csys =  0.07 CPU)

What just happened?

Because Queue::Word::Test is a subclass of Queue::Test, it inherits all the test methods from Queue::Test. It must override setup_queues() so that the fixture creates objects of the proper class, though.

There’s no practical benefit in rewriting the tests for size() and dequeue(), as the subclass does not change their behavior. The enqueue() method, however, is more restrictive with its arguments. check_only_words() tests that the program dies when it receives invalid arguments.

Calling runtests() tells Test::Class to run all tests in both loaded test classes. Because the test subclass adds additional testing methods, the queue_word.t test file runs more tests than did the queue.t test file.

Skipping Tests with Test::Class

If you need to skip the tests for a class, you might want to skip the tests for any of its subclasses as well. If you’ve set up your test class hierarchy to mimic your real class hierarchy, this is easy to do.

How do I do that?

"Inheriting Tests" showed how to set up tests for the Queue::Word module and its parent class, Queue. Similarly, the test classes for these modules were Queue::Word::Test and Queue::Test, respectively. Suppose that your project lead won’t let you run the tests for Queue::Test and any of its subclasses after four o’clock because he doesn’t believe you’ll have time to fix them before you leave for the day.

Alter Queue/Test.pm as follows:

    package Queue::Test;
    
    use base 'Test::Class';
    
    use Queue;
    use Test::More;
    
    sub SKIP_CLASS
    {
        return [ localtime(time) ]->[2] < 16 ? 0 : 'only runs before tea time';
    }
    
    sub setup_queues : Test( setup => 2 )
    {
       #  ...
    }

Run queue.t with prove after four o’clock to see that it skips tests in both Queue::Test and Queue::Word::Test:

    $ prove -v queue.t
    queue....1..2
    ok 1 # skip only runs before tea time
    ok 2 # skip only runs before tea time
    ok
            2/2 skipped: only runs before tea time
    All tests successful, 2 subtests skipped.
    Files=1, Tests=2,  0 wallclock secs ( 0.05 cusr +  0.00 csys =  0.05 CPU)

What about...

Q: Can I skip tests for just one particular class?

A: Sure. Instead of overriding the SKIP_CLASS() method, simply call it on your class and pass it the reason for skipping the tests. Perhaps you want to to skip the tests for Queue::Test if they run in the morning, but you don’t want to affect its subclasses. Modify Queue/Test.pm as follows:

    package Queue::Test;
    
    use base 'Test::Class';
    
    use Queue;
    use Test::More;
    
    Queue::Test->SKIP_CLASS(
        [ localtime(time) ]->[2] <= 12
        ? 'only runs in the afternoon'
                        : 0
                        );
    
    sub size : Test(4)
    {
       #  ...
    }

Marking Tests as TODO with Test::Class

If you’ve written the tests for a class but you haven’t yet written the implementation, mark the tests as TODO. That way, everyone will know that you expect them to fail. If they succeed, it’ll be a nice surprise.

How do I do that?

Test::Class allows you to mark tests in the same manner as tests using Test::More. Simply localize the $TODO variable with the reason why you’re putting them off.

Ponder yet again the Queue module and its test module, Queue::Test, from "Writing Test Cases.” Imagine that your boss wants you to modify enqueue() to refuse to queue undefined values. It’s 4:45 p.m. and you want to code the tests so you’ll remember your brilliant idea in the morning. Modify Queue/Test.pm as follows:

    sub enqueue : Test(3)
    {
        my $queue = Queue->new;
        isa_ok( $queue, 'Queue' );
    
        $queue->enqueue($_) for qw( howdy bonjour );
        is( $queue->size(), 2, 'queue is now larger' );
    
        local $TODO = 'decided to disallow undefined items';
        $queue->enqueue(undef);
        is( $queue->size(), 2, "queue size hasn't changed" );
    }

Run queue.t to show that the test fails but has a TODO declaration, just as do the regular TODO tests of Test::More. Now you can go home, confident that you will remember what Queue.pm has to do when you return to work in the morning.

What about...

Q: Can I mark an entire class as TODO?

A: Unfortunately, Test::Class doesn’t provide a simple way to do this. It’s probably easier just to skip the tests (see "Skipping Tests with Test::Class,” earlier in this chapter).



[1] Of course, you might worry if she could see the paper clip trebuchet you’ve been using to fire paper clips at coworkers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.80.34