A Test::MockObject Illustrated Example

People like to find excuses to avoid writing tests for their code. One of the most common goes something like, "It's not feasible to test this, because it relies on external objects" - CGI code, code using the Apache request object, TCP/IP servers, and so on.

The Test::MockObject module makes it much easier to isolate code that uses such objects. For example, if your code uses a CGI object, then you could fiddle with query strings and faked STDIN, trying to persuade CGI.pm to produce testable values. It's easier to use Test::MockObject to create an object that looks and behaves like a CGI object -- but that is completely under your control.

This comes in handy in large software projects, where objects encapsulate tremendous amounts of hidden behavior. If your application follows good design principles by hiding complexity behind well-defined interfaces, then you can replace nearly any component with its interface equivalent. (The internals, of course, are free to change. That's why this is possible.) Often, it's sufficient just to accept certain arguments and to return specific values.

Using a mock object, you can test whether the code in question uses a particular interface correctly. It's possible to do this by hand, but Test::MockObject has several utility methods to add fake methods and verify calls. It integrates with Test::Builder, so it works correctly with Test::Simple, Test::More, and their cousins.

The Background You Need to Know

I assume that you are already familiar with Test::More. Perhaps you've read Test::Tutorial, which comes with the Test::Simple distribution. You may also have read my earlier introduction to the subject. If not, then you may wish to do so. (My roommate tried it out of order and hurt his head. If you fare any better, then the Perl QA group is interested in your natural talent!)

My example comes from the unit tests for the Everything Engine. I chose it for two reasons. First, it's a project near and dear to my heart. Second, it needs more users, testers and developers. More importantly, it's where I came up with the ideas that led to Test::MockObject.

My colleague on the Engine, Darrick Brown, devised a clever technique he dubbed Form Objects. These are used to bind menu choices to nodes. (Everything in Everything is a node. It's the base unit of data and behavior.) Form objects control the creation of HTML widgets, verify submitted data and ultimately update nodes. They all inherit strongly from Everything::FormObject and operate on node objects, so they're an ideal candidate for mock objects.

Mock Objects

This article focuses on white-box unit testing with mock objects. "White box" testing means that you're allowed and encouraged to look at the internals of the thing being tested. This is scarily possible, with Perl. By contrast, "black box" testing happens when you cannot know the internal details: you just know allowed inputs and the expected outputs. (If you don't know that, then you can't do much testing.)

Unit testing, of course, is testing individual components of the program in isolation, as far as possible. This is different from integration testing, which exercises the program as a whole, and acceptance testing, which explores the desired end-user behavior of the program. No type of testing is better or worse than any other. Done properly, they are complementary: Unit tests are capable of exploring internal behaviors that are difficult to prove with acceptance tests; integration tests demonstrate the interoperability between different components that unit tests usually cannot guarantee.

The points of a mock object is to isolate the units being tested from their dependencies, and to give testers more complete control over the testing environments. This follows from standard programming principles: If you can fake the interfaces your unit relies on, then you can control and monitor its behavior.

Perl makes this particularly easy.

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?)

Testing an Actual Method

Once the module is loaded and ready, I like to test my methods in the order in which they appear. This helps to keep the test suite and the module somewhat synchronized. The first method is genObject(). It's fairly simple:


        my $this = shift @_;
        my ($query, $bindNode, $field, $name, $default) =
                getParamArray(
                "query, bindNode, field, name, 
                     default", @_);
        
        $name ||= $field;
        
        my $html = $this->SUPER::genObject(
                $query, $bindNode, $field, $name) . 
                     "\n";
        
        if(ref $bindNode)
        {
                my $author = $DB->getNode($$bindNode{$field});
                if($author && $author->isOfType('user'))
                {
                        $default ||= $$author{title};
                }
                elsif($author)
                {
                        $default ||= "";
                }
        }

        $html .= $query->textfield(-name => $name, 
        	    -default => $default,
                -size => 15, -maxlength => 255,
                -override => 1) . "\n";
                
        return $html;

I can see several spots that need tests. First, I want to make sure that getParamArray() is called with the proper arguments. (This function makes it possible to pass parameters by position or in name => value pair style, similar to Sub::NamedParams.) Next, I'll test that SUPER::genObject() is called correctly, with the proper values. (This call looks odder than it is, due to the way the Engine handles node inheritance. For a good time, read Everything::Node::AUTOLOAD(), or take up bowling.) After that, there's the conditional statement. I'll have to call the function at least three times to test the branches effectively. The function ends with a textfield() call I want to test, and has a return value where I can check some other things. It's not as complex as it looks.

One of the side benefits of testing is that you'll start to write smaller and smaller functions. This is especially evident if you write tests before you write code to pass them. Besides being easier to read and debug, this tends to produce code that's more flexible and much more powerful.

Having identified several things to test, I next write test names. These are short descriptions of the intent of the tests. When confronted with a piece of existing code, I usually try to figure out what kinds of things can break, and what the important behavior really is. With experience, you can look at a piece of well-written code and figure it out intuitively. There's room to be explicit when you're just starting, though.


        # genObject() should call getParamArray()
        # ... passing a string of desired parameters
        # ... and its arguments, minus the object
        # ... should call SUPER::genObject()
        # ... passing the important parameters
        # ... and should call textfield()
        # ... with the important parameters
        # ... returning its and its parents results
        # ... if node is bound, should call getNode()
        # ... on bound node field
        # ... checking for an author node
        # ... and setting the default to the author name
        # ... or nothing, if it is not an author node

That's a pretty good rough draft. Going through the list reveals the need to make at least two more passes through the code. Getting this in the right order takes a little work, but once you know how to set up the mocking conditions correctly, it's really fast and easy. The best way I've found to handle this is just to jump right in.


        can_ok( 'Everything::HTML::FormObject::AuthorMenu', 
        	'genObject' );

First, I want to make sure this method exists. Why? It's part of the ``Do the simplest thing that could possibly work'' principle. Whenever I add a method, I first check to see whether it exists. It sounds too stupid to have any use, but this is a thing that could possibly break. First, I've been known to misspell method names occasionally. This'll catch that immediately. Second, it gives me a place to start and a test that passes with little work. That's a nice psychological boost that moves me on to the next test. I've kept this habit when writing tests for existing code.

Next, I want to test the call to getParamArray(). Since it's not a method, I can't use a mock object. I'll have to mock it sideways. Though the function comes from Everything.pm, it would normally be exported into this package. I'll use a variant of the import()-mockery earlier:


        my ($gpa, @gpa);
        $mock->fake_module( $package,
            getParamArray => sub { push @gpa, \@_; return $gpa }
        );

I can count the elements of @gpa to see whether it was called, and pull the arguments out of the array. $gpa allows me to control the output. The test itself is easy to write:


        my $result = genObject();
        is( @gpa, 1, 'genObject() should call getParamArray' );

OK, it's a little easier to write than it should have been. If you're paying attention, then you should wonder why the genObject() call works, as I'm still in the main package and the method is in the class. I've just added a variable with the tested package name, as well as an AUTOLOAD() function. I'm already tired of typing the big long package name:


        # near the start
        use vars qw( $AUTOLOAD );
        my $package = 'Everything::HTML::FormObject::AuthorMenu';
        
        ...
        
        # way down at the end
        sub AUTOLOAD {
                my ($subname) = $AUTOLOAD =~ /([^:]+)$/;
                if (my $sub = UNIVERSAL::can( $package,
                     $subname )) {
                        $sub->( @_ );
                } else {
                        warn "Cannot call <$subname> 
                             in ($package)\n";
                }
        }

With all of that infrastructure in place, it's a little disappointing to realize that the test dies. Since I'm calling the method as a function, there's no object on which to call SUPER::genObject(). This is easily solved. Remember that $mock (a mock object) was created earlier? Here's one bit of Perl's magic that drives some OO purists crazy, but makes it oh-so-easy to test. A method call is a function call with a special first argument. If $this, inside genObject(), can do everything that an Everything::HTML::FormObject::AuthorMenu object can do, then it'll just work. Hooray for polymorphism!

To make the SUPER::genObject() call pass, that call will also have to be mocked. The method resolves to Everything::HTML::FormObject::genObject(), so I'll add a function of the appropriate name and package. (This test code is starting to look familiar. Again, Test::MockModule, anyone?)


        my @go;
        $mock->fake_module( 'Everything::HTML::FormObject',
            genObject => sub { push @go, \@_; 
                 return 'some html' }
        );

Now I modify the genObject() call, passing in my mock object:


        my $result = genObject( $mock );

I get further before things fail. Since there's nothing passed in for the $query variable to hold, the textfield() call fails. Now I can finally use my mock object to good effect. First, I'm going to change what getParamArray() returns, using $package again to save on typing:


        my (%gpa, @gpa);
        $mock->fake_module( $package,
            getParamArray => sub { push @gpa, \@_; return 
            @gpa{qw( q bn f n d )}}
        );

Since AuthorMenu expects to receive its arguments in order, I'll create a hash where I can store them. I might use more descriptive key names, but they seem to make sense now. Next, I'll make sure that 'q' returns something controllable. In this case, that means that it supports the textfield() method and returns something sane:


        $mock->set_always( 'textfield', 'more html' );
        $gpa{q} = $mock;

I could create a new mock object for this, but since there's no name collision yet, it's not a big priority. Whether you do this is a matter of personal style.

For now, genObject() does not die, and all tests pass. Whew. Next up, I test to see whether the first argument to getParamArray() is correct.


        is( $gpa[0][0], 'query, bindNode, field, name, default',
            '... requesting the appropriate arguments' );

It is, so I'll make sure that it passes along the rest of the method arguments, minus the object itself:


        like( join(' ', @{ $gpa[0] }), qr/1 2 3$/,
                '... with the method arguments' );
        unlike( join(' ', @{ $gpa[0] }), qr/$mock/,
                '... but not the object itself' );

Only the first of these fails, and that's because I'm not passing any other arguments to the method call. I'll modify it:


        my $result = genObject( $mock, 1, 2, 3 );

This gives me nine tests that pass. I'm also following the test names fairly closely. (In between writing those and actually writing this code, a couple of days passed. Their similarities make me think I'm on the right track.)

Since the next piece of the code tries to load a bound node and I'm not passing one in, I'll test to see that getNode() is not called. Since the call is on the $DB object, I'll set it to the mock object. I'll also use the called() method to make sure that nothing happens. For that to happen, I need to mock getNode(). The code to implement all of this is pretty simple. (Note that it must go in various places):


        $Everything::HTML::FormObject::AuthorMenu::DB = $mock;
        $mock->set_series( 'getNode', 0, 1, 2 );

        # genObject() calls skipped in this example...

        ok( ! $mock->called( 'getNode' ),
                '... should not fetch bound node without one' );

Two things bear more explanation. First, since I don't really know what I want getNode() to return, I'll give it a dummy series. (I'm pretty sure I'll be using set_series() on it, because I've done tests like this before. I can't explain it much beyond intuitive experience.) Second, I'm negating the return value of called() so it will fit with ok(). This can be a little hard to see, sometimes.

The last few tests of the first pass all revolve around the textfield() call. I've already mocked it, so now I'll see whether it was called with the correct arguments:


        my ($method, $args) = $mock->next_call();
        shift @$args;
        my %args = @$args;

        is( $method, 'textfield', '... and should create 
             a text field' );
        is( join(' ', sort keys %args ),
        join(' ', sort qw( -name -default -size 
             -maxlength -override )), '... passing the 
             essential arguments' );

Several bits here stick out. The next_call() method iterates through the call stack of mocked methods, in order. It doesn't track every method called, just the ones you've added through one of Test::MockObject's helper methods. next_call() returns the name of the method (in scalar context) or the name of the method an an anonymous array containing the arguments (in list context).

Since I want to check the arguments passed to textfield(), I call it in list context. Because the arguments are passed as key => value pairs, the most natural comparison seems to be as a hash. I use the join-sort idiom quite often, as I've never quite been comfortable with the list comparison functions of Test::More. This test would probably be much simpler if I used them.

I explicitly sort both arrays just so a hardcoded list order won't cause unnecessary test failures. (This has bitten me when writing code that ought to work on EBCDIC machines, not just ASCII ones. Of course, if you get Everything up and going on a mainframe, then this is probably the least of your concerns.)

Finally, I test to see whether the returned data is created properly:


        is( $result, "some html\nmore html\n",
            '... returning the parent object plus the 
             new textfield html' );

So far, all 13 of the tests succeed. At this point, I started my second pass through the method, but noticed that I hadn't yet tested that $name would get its default value from $field. I'll add 'field' to %gpa before the first pass:


        $gpa{f} = 'field';

This test ought to go before the final test in this pass, so I add it there, too. This finishes the first pass:


        is( $args{-name}, 'field',
                '... and widget name should default 
                to field name' );

For the second pass, I will test what happens when I provide a node to which to bind the form object. In an unmocked Everything, this node comes as an argument to the function. In the tests, I must return them from the mocked getParamArray(), so that's where I will start. I also set the 'field' value in the mock object to a sentinel value I'll check for later. Since the value of $field will be 'field,' it works out nicely. (There's room to be much more creative on these names, especially if you're trying to sneak the name of a friend into your software.)


        $gpa{bn} = $mock;
        $mock->{field} = 'bound node';

Because getNode() has a series set on it and hasn't been called before, it will return 0. That means that isOfType() won't be called on the author object, and the default choice won't be modified from its undefined value. These tests are fairly easy:


        genObject( $mock );
        
        ($method, $args) = $mock->next_call();
        isnt( $method, 'isOfType',
        '... not checking bound node type if it is not found' );
        
        shift @$args;
        %args = @$args;
        is( $args{-default}, undef, '... and not modifying 
             default selection' );
            

As before, next_call() comes in handy. Since I already know that textarea() will be the first (and last, for this pass) method called, I can make sure that isOfType() was not called.

Two tests follow. One ensures that the code checks the node's type. The other makes sure that the default value becomes a blank string if the type is incorrect. To make this work, I had to modify the existing getNode() series to return two instances of $mock.


        $mock->{title} = 'bound title';
        $mock->set_series( 'isOfType', 0, 1 );
        
        genObject( $mock );
        
        ($method, $args) = $mock->next_call( 2 );
        is( $method, 'isOfType', '... if bound node 
            is found, should check type' );
        is( $args->[1], 'user', '... (the user type)' );
        
        ($method, $args) = $mock->next_call();
        shift @$args;
        %args = @$args;
        is( $args{-default}, '',
            '... setting default to blank string 
                 if it is not a user' );

The only new idea here is of passing an argument to next_call(). I know getNode() is the first mocked method, so I can safely skip it. These tests all pass. The final testable condition is where the bound node exists and is a 'user' type node. The set_series() call in the last test block makes isOfType() return true for this pass:


        genObject( $mock );
        ($method, $args) = $mock->next_call( 3 );
        shift @$args;
        %args = @$args;
        is( $args{-default}, 'bound title', '... but using 
             node title if it is' );

I now have 22 successful tests. My original test name plan had 14 tests, but that number generally grows as I see more logic branches. I could add more tests, making sure that default values are not overwritten, and that the essential (hardcoded) attributes of the textfield() are set, but I'm reasonably confident in the tests as they stand. If something breaks, then I'll add a test to catch the bug before I fix it, but what's left is simple enough; I doubt it will break. (Writing that is a good way to have to eat my words later.)

Testing Another Method (A Less Detailed Example)

One method remains for this form Object: cgiVerify(). When the Engine processes input from submitted Form Object forms, it must rebuild the objects. Then, it checks the input against allowed values for the widgets. This method does just that. Its code is slightly longer, and reads:


        my ($this, $query, $name, $USER) = @_;
        
        my $bindNode = $this->getBindNode($query, $name);
        my $author = $query->param($name);
        my $result = {};
        
        if($author)
        {
                my $AUTHOR = $DB->getNode($author, 'user');
                
                if($AUTHOR)
                {
                        # We have an author!!  Set the CGI param 
                        # so that the
                        # inherited cgiUpdate() will just do 
                        # what it needs to!
                        $query->param($name, $$AUTHOR{node_id});
                }
                else
                {
                        $$result{failed} = "User '$author' 
                             does not exist!";
                }
        }
        
        if($bindNode)
        {
                $$result{node} = $bindNode->getId();
                $$result{failed} = "You do not have permission"
                        unless($bindNode->hasAccess($USER, 'w'));
        }
        
        return $result;

Rather than describing the writing of the tests (and my steps and missteps therein), I'll just comment on the tests themselves.


        my $qmock = Test::MockObject->new();
        $mock->set_series( 'getBindNode', 0, 0, 0, 
             $mock, $mock );
        $qmock->set_series( 'param', 0, 'author', 'author' );

Because of the way this method handles things, it's clearer to create another mock object to pass in as $query. I'm also setting up the two main series used for the several passes through this method. While writing the tests, I kept adding new values to these series. This is what remained at the end. This approach makes more sense to me than setting each mock before each pass, but it's a matter of style, and either way will work.


        $result = cgiVerify( $mock, $qmock, 'boundname' );

        ($method, $args) = $mock->next_call();
        is( $method, 'getBindNode', 'cgiVerify() should get 
             bound node' );
        is( join(' ', @$args), "$mock $qmock boundname",
                '... with query object and query 
                     parameter name' );

Here's the reason I used separate mock objects: to tell them apart as arguments in the getBindNode() call.


        ($method, $args) = $qmock->next_call();
        is( $method, 'param', '... fetching parameter' );
        is( $args->[1], 'boundname', '... by name' );

        isa_ok( $result, 'HASH', '... and should return a data 
             structure which' );

The weird test name here is another of my little idioms. isa_ok() adds 'isa (reference type)' to the end of its test names, and I want them to be as clear as possible when they're displayed.


        $mock->set_series( 'getNode', 0, { node_id => 
             'node_id' } );
        $result = cgiVerify( $mock, $qmock, 'boundname' );
        
        ($method, $args) = $mock->next_call( 2 );
        is( $method, 'getNode', '... fetching the node, if an 
             author is found' );
        is( join(' ', @$args), "$mock author user",
                '... with the author, for the user type' );
                
        is( $result->{failed}, "User 'author' does not exist!",
                '... setting a failure message on failure' );

I like the approach of joining the arguments in a string and doing an is() or a like() call on them. The benefit of like() is that you can ignore the $self passed as the first argument, because it's the mock object. I used is() here to make it more explicit what I'm expecting.


        $qmock->clear();
        $result = cgiVerify( $mock, $qmock, 'boundname' );
        ($method, $args) = $qmock->next_call( 2 );
        is( $method, 'param', '... setting parameters, 
             on success' );
        is( join(' ', @$args), "$qmock boundname node_id",
             '... with the name and node id' );
        is( $result->{failed}, undef, 
             '... and no failure message' );

This bit of code gave me trouble until I added the clear() call. It's worth remembering that a mock object's stack of mocked calls persists through passes. I had forgotten that, and was using the first param() call instead of the second. Oops.

Another thing worth noting is that I pass 'undef' as the expected result to is(). Conveniently, Test::More silently does the right thing.


        $mock->set_always( 'getId', 'id' );
        $mock->set_series( 'hasAccess', 0, 1 );
        $result = cgiVerify( $mock, $qmock, 
             'boundname', 'user' );

Here, I exercise the method's final clause. These tests are more complex than the code being tested! Sometimes, it works out that way.


        $mock->called_pos_ok( -2 , 'getId',
                '... should get bound node id, if it exists' );
        is( $result->{node}, 'id',
                '... setting it in the resulting node field' );
                
        $mock->called_pos_ok( -1, 'hasAccess', 
             '... checking node access ');
        is( $mock->call_args_string( -1, ' ' ), 
             "$mock user w", 
             '... for user with write permission' );

I've moved away from the next_call() approach to the older Test::MockObject behavior of using positions in the call stack. I'm still not quite pleased with the names of these methods, but the negative subscripts are handy. (Maybe I need to add prev_call()). All I have to do is remember that hasAccess() is called last, and that getId() should be called as the second-to-last method.

The other new method here is call_args_string(), which simply joins the arguments at the specified call position together. It saves a bit of typing, most of which is offset by the long method name.



        is( $result->{failed}, 'You do not have permission',
                '... setting a failure message if user lacks 
                 write permission' );

        $result = cgiVerify( $mock, $qmock, 'boundname', 'user' );
        is( $result->{failed}, undef, '... and none if the user 
             has it' );

These final two tests demonstrate how, at the end of a long series of tests, the available options are whittled down. By the final couple of passes, I'm generally testing only one thing at a time. That always seems mathematically poetic, to me, as if I'm refining with Newton's method.

Conclusion

This whole exercise has produced 39 tests. My next step is to update the test plan with this information.


        # way back at the top
        use Test::More tests => 39;

This makes it easier to see whether too many or too few tests were run. I get better results about failures and successes this way, too. As it turns out, writing the tests for cgiVerify() took about 20 minutes, give or take some laundry-related distractions. That seems about right for 17 tests on code I haven't read in several months -- about one a minute, when you know what you're doing.

It's worth noting the features of this module that lead me to consider mock objects. Mostly, the process of fetching and building nodes is complex enough that I didn't really want to hook up a fake database, just so I could go through all of the code paths required to test that an author is really an author. If the code had gone through some simple mathematical or textual manipulations, then I would probably have used black-box testing. Code that relies on a database abstraction layer (as does Everything) generally makes me reach for Test::MockObject, however.

For more information on what the module can do, please see its documentation. The current stable version is 0.08, though 0.09 will probably have been released by the time this article is stable.

chromatic is the author of Modern Perl. In his spare time, he has been working on helping novices understand stocks and investing.

Visit the home of the Perl programming language: Perl.org

Sponsored by

Monthly Archives

Powered by Movable Type 5.13-en