+ 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".
+
+
+