Sign In/My Account | View Cart  
advertisement


Listen Print

A Test::MockObject Illustrated Example
by chromatic | Pages: 1, 2, 3, 4, 5, 6

The Example

I'm writing a test for Everything::HTML::FormObject::AuthorMenu. This class represents an HTML select box used to set the author of a node. It has two methods. genObject() produces the HTML necessary for the select widget. It is called when the Engine builds a page to display to the user. The other method, cgiVerify(), is called when receiving data from a user submission. It checks to see whether the requested author exists and has write access to the node.

Looking Inside

The module begins rather simply:


        use strict;
        use Everything;
        use Everything::HTML;
        use Everything::HTML::FormObject;

        use vars qw(@ISA);
        @ISA = ("Everything::HTML::FormObject");

Testing this is all very easy. I'd like to make sure that the module continues to load all of these modules, so I need some way to catch their use. (Don't laugh -- I've forgotten to load important modules before, causing really tricky errors. It's better to be precise. Now you can laugh.) Because use calls import() behind the scenes, if I can install my own import() before AuthorMenu is compiled, then I can test that these modules are actually used. As a side benefit, doing so prevents these other classes from loading, making it easier to mock them. The test starts:


        package Everything::HTML::FormObject::AuthorMenu;
        use vars qw( $DB );

        package main;

        use Test::More 'no_plan';

Because I'm faking the other packages, anything the true modules would normally import will not be imported. The only thing that really matters at this point is the $DB object, exported from the Everything package. (I'm cheating a little bit. I know I'll use it later, and I know where it's defined and how it's used. At this point, I should probably say, ``The module will fail to compile unless I fake it here,'' and leave it at that.)

Because I'm not ready to implement $DB, I just switch to the package to be tested and declare it as a global variable. When the package is compiled, it's already there, so it'll compile successfully. Then I return to the main package so I don't accidentally clobber anything important and use the testing module.


        my @imports;
        for ( 'Everything', 'Everything::HTML',
                'Everything::HTML::FormObject') {
                no strict 'refs';
                *{ $_ . '::import' } = sub {
                        push @imports, $_[0];
                };
                (my $modpath = $_) =~ s!::!/!g;
                $INC{ $modpath . '.pm' } = 1;
        }

Here's where it starts to get tricky. Because I want to ensure that these three modules are loaded correctly, I have to make Perl think they're already loaded. %INC comes to the rescue. When you use() or require() a module successfully, Perl adds an entry to %INC with the module pathname, relative to one of the directories in @INC. That way, if you use() or require() the module again, then Perl knows that it's already been loaded.

As mentioned before, my preferred way to check that a module is loaded is to trap all calls to import(). That's why I install fake import subroutines. They simply save the name of the package by which they're identified. The tests are simple to write:


        use_ok( 'Everything::HTML::FormObject::AuthorMenu' );
        
        is( $imports[0], 'Everything', 'Module should use 
        	Everything' );
        is( $imports[1], 'Everything::HTML',
                'Module should use Everything::HTML' );
        is( $imports[2], 'Everything::HTML::FormObject',
                'Module should use 
                     Everything::HTML::FormObject' );

Because use_ok() fires at runtime, it's safe not to wrap this whole section in a BEGIN block. (If you're curious about this, see what perlfunc has to say about use() and its equivalent.)

That works, but it's a little messy and uses some tricks that might scare (or at least confuse) the average Perl hacker. One of the goals of the Test::* modules is to do away with the ``evil black magic'' you'd normally have to use. So, now I show you a more excellent way.

Making That Last Bit Easier

Having found myself writing that code way too often (at least twice), I added it to Test::MockObject. Using the module, the corresponding loop is now:


        my @imports;
        for ( 'Everything', 'Everything::HTML',
                'Everything::HTML::FormObject') {
                Test::MockObject->fake_module(
                        $_,
                        import => sub { push @imports,
                              $_[0] }
                );
        }

Behind the scenes, the module does exactly what the loop did. The nice thing is that you don't have to remember how to fake that a module is loaded or how to test import(). It's already done and it's nicely encapsulated in a module. (I inadvertently drove this point home to myself when writing this section. It turns out that version 0.04 of Test::MockObject populated %ENV instead of %INC. I make that typo often. This time, it was in both the module and the test. Get at least version 0.08, as this article has led to bug fixes and new conveniences. :)

This isn't the most important feature of Test::MockObject, though. It's just a convenient addition. At some point, it should probably be spun off into Test::MockPackage or Test::MockModule. (Want to contribute?)

Pages: 1, 2, 3, 4, 5, 6

Next Pagearrow