+ This was originally a pattern named by Robert Binder (Testing + object-oriented systems: models, patterns, and tools, + Addison-Wesley) in 1999. + A server stub is a simulation of an object or component. + It should exactly replace a component in a system for test + or prototyping purposes, but remain lightweight. + This allows tests to run more quickly, or if the simulated + class has not been written, to run at all. +
+ + ++ All we need is an existing class, say a database connection + that looks like this... +
+class DatabaseConnection { + function DatabaseConnection() { + } + + function query() { + } + + function selectQuery() { + } +} ++ The class does not need to have been implemented yet. + To create a stub version of the class we need to include the + server stub library and run the generator... +
+require_once('simpletest/mock_objects.php'); +require_once('database_connection.php'); +Stub::generate('DatabaseConnection'); ++ This generates a clone class called + StubDatabaseConnection. + We can now create instances of the new class within + our prototype script... +
+require_once('simpletest/mock_objects.php'); +require_once('database_connection.php'); +Stub::generate('DatabaseConnection'); + +$connection = new StubDatabaseConnection(); + ++ The stub version of a class has all the methods of the original + so that operations like + $connection->query() are still + legal. + The return value will be null, + but we can change that with... +
+$connection->setReturnValue('query', 37) ++ Now every time we call + $connection->query() we get + the result of 37. + We can set the return value to anything, say a hash of + imaginary database results or a list of persistent objects. + Parameters are irrelevant here, we always get the same + values back each time once they have been set up this way. + That may not sound like a convincing replica of a + database connection, but for the half a dozen lines of + a test method it is usually all you need. + + + +
+ Things aren't always that simple though. + One common problem is iterators, where constantly returning + the same value could cause an endless loop in the object + being tested. + For these we need to set up sequences of values. + Let's say we have a simple iterator that looks like this... +
+class Iterator { + function Iterator() { + } + + function next() { + } +} ++ This is about the simplest iterator you could have. + Assuming that this iterator only returns text until it + reaches the end, when it returns false, we can simulate it + with... +
+Stub::generate('Iterator'); + +$iterator = new StubIterator(); +$iterator->setReturnValue('next', false); +$iterator->setReturnValueAt(0, 'next', 'First string'); +$iterator->setReturnValueAt(1, 'next', 'Second string'); ++ When next() is called on the + stub iterator it will first return "First string", + on the second call "Second string" will be returned + and on any other call false will + be returned. + The sequenced return values take precedence over the constant + return value. + The constant one is a kind of default if you like. + +
+ Another tricky situation is an overloaded + get() operation. + An example of this is an information holder with name/value pairs. + Say we have a configuration class like... +
+class Configuration { + function Configuration() { + } + + function getValue($key) { + } +} ++ This is a classic situation for using stub objects as + actual configuration will vary from machine to machine, + hardly helping the reliability of our tests if we use it + directly. + The problem though is that all the data comes through the + getValue() method and yet + we want different results for different keys. + Luckily the stubs have a filter system... +
+Stub::generate('Configuration'); + +$config = &new StubConfiguration(); +$config->setReturnValue('getValue', 'primary', array('db_host')); +$config->setReturnValue('getValue', 'admin', array('db_user')); +$config->setReturnValue('getValue', 'secret', array('db_password')); ++ The extra parameter is a list of arguments to attempt + to match. + In this case we are trying to match only one argument which + is the look up key. + Now when the server stub has the + getValue() method invoked + like this... +
+$config->getValue('db_user'); ++ ...it will return "admin". + It finds this by attempting to match the calling arguments + to its list of returns one after another until + a complete match is found. + +
+ You can set a default argument argument like so... +
+ +$config->setReturnValue('getValue', false, array('*')); ++ This is not the same as setting the return value without + any argument requirements like this... +
+ +$config->setReturnValue('getValue', false); ++ In the first case it will accept any single argument, + but exactly one is required. + In the second case any number of arguments will do and + it acts as a catchall after all other matches. + Note that if we add further single parameter options after + the wildcard in the first case, they will be ignored as the wildcard + will match first. + With complex parameter lists the ordering could be important + or else desired matches could be masked by earlier wildcard + ones. + Declare the most specific matches first if you are not sure. + +
+ There are times when you want a specific object to be + dished out by the stub rather than just a copy. + The PHP copy semantics force us to use a different method + for this. + You might be simulating a container for example... +
+class Thing { +} + +class Vector { + function Vector() { + } + + function get($index) { + } +} ++ In this case you can set a reference into the stub's + return list... +
+Stub::generate('Vector'); + +$thing = new Thing(); +$vector = &new StubVector(); +$vector->setReturnReference('get', $thing, array(12)); ++ With this arrangement you know that every time + $vector->get(12) is + called it will return the same + $thing each time. + +
+ These three factors, timing, parameters and whether to copy, + can be combined orthogonally. + For example... +
+$complex = &new StubComplexThing(); +$stuff = new Stuff(); +$complex->setReturnReferenceAt(3, 'get', $stuff, array('*', 1)); ++ This will return the $stuff only on the third + call and only if two parameters were set the second of + which must be the integer 1. + That should cover most simple prototyping situations. + +
+ A final tricky case is one object creating another, known + as a factory pattern. + Suppose that on a successful query to our imaginary + database, a result set is returned as an iterator with + each call to next() giving + one row until false. + This sounds like a simulation nightmare, but in fact it can all + be stubbed using the mechanics above. +
++ Here's how... +
+Stub::generate('DatabaseConnection'); +Stub::generate('ResultIterator'); + +class DatabaseTest extends UnitTestCase { + + function testUserFinder() { + $result = &new StubResultIterator(); + $result->setReturnValue('next', false); + $result->setReturnValueAt(0, 'next', array(1, 'tom')); + $result->setReturnValueAt(1, 'next', array(3, 'dick')); + $result->setReturnValueAt(2, 'next', array(6, 'harry')); + + $connection = &new StubDatabaseConnection(); + $connection->setReturnValue('query', false); + $connection->setReturnReference( + 'query', + $result, + array('select id, name from users')); + + $finder = &new UserFinder($connection); + $this->assertIdentical( + $finder->findNames(), + array('tom', 'dick', 'harry')); + } +} ++ Now only if our + $connection is called with the correct + query() will the + $result be returned that is + itself exhausted after the third call to next(). + This should be enough + information for our UserFinder class, + the class actually + being tested here, to come up with goods. + A very precise test and not a real database in sight. + + + +
+ There are some additional options when creating stubs. + At the generation stage we can change the class name... +
+Stub::generate('Iterator', 'MyStubIterator'); +$iterator = &new MyStubIterator(); + ++ This is not very useful in itself as there would be no difference + in this class and the default except for the name. + However we can also add additional methods not found in the + original interface... +
+class Iterator { +} +Stub::generate('Iterator', 'PrototypeIterator', array('next', 'isError')); +$iterator = &new PrototypeIterator(); +$iterator->setReturnValue('next', 0); + ++ The next() and + isError() methods can now have + return values set just as if they existed in the original class. + +
+ One other esoteric way of customising the stubs is to change + the default wildcard used for parameter matching. +
+Stub::generate('Connection'); +$iterator = &new StubConnection('wild'); +$iterator->setReturnValue('query', array('id' => 33), array('wild')); + ++ The only reason to do this is if you genuinely wanted to test + against the literal string "*" and didn't want it + interpreted as "any". + + +