From 322bb9cd2be9e51422cb2b82684692e825c2bfb7 Mon Sep 17 00:00:00 2001 From: brettp Date: Fri, 2 Oct 2009 18:40:04 +0000 Subject: Added simpletest and start of unit tests. git-svn-id: http://code.elgg.org/elgg/trunk@3503 36083f99-b078-4883-b0ff-0f9b5a30f544 --- .../docs/en/mock_objects_documentation.html | 757 +++++++++++++++++++++ 1 file changed, 757 insertions(+) create mode 100755 vendors/simpletest/docs/en/mock_objects_documentation.html (limited to 'vendors/simpletest/docs/en/mock_objects_documentation.html') diff --git a/vendors/simpletest/docs/en/mock_objects_documentation.html b/vendors/simpletest/docs/en/mock_objects_documentation.html new file mode 100755 index 000000000..c3d002277 --- /dev/null +++ b/vendors/simpletest/docs/en/mock_objects_documentation.html @@ -0,0 +1,757 @@ + + + +SimpleTest for PHP mock objects documentation + + + + +

Mock objects documentation

+ This page... + +
+

What are mock objects?

+

+ Mock objects have two roles during a test case: actor and critic. +

+

+ The actor behaviour is to simulate objects that are difficult to + set up or time consuming to set up for a test. + The classic example is a database connection. + Setting up a test database at the start of each test would slow + testing to a crawl and would require the installation of the + database engine and test data on the test machine. + If we can simulate the connection and return data of our + choosing we not only win on the pragmatics of testing, but can + also feed our code spurious data to see how it responds. + We can simulate databases being down or other extremes + without having to create a broken database for real. + In other words, we get greater control of the test environment. +

+

+ If mock objects only behaved as actors they would simply be + known as server stubs. + 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. +

+

+ However, the mock objects not only play a part (by supplying chosen + return values on demand) they are also sensitive to the + messages sent to them (via expectations). + By setting expected parameters for a method call they act + as a guard that the calls upon them are made correctly. + If expectations are not met they save us the effort of + writing a failed test assertion by performing that duty on our + behalf. +

+

+ In the case of an imaginary database connection they can + test that the query, say SQL, was correctly formed by + the object that is using the connection. + Set them up with fairly tight expectations and you will + hardly need manual assertions at all. +

+ +

Creating mock objects

+

+ In the same way that we create server stubs, 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 mock version of the class we need to include the + mock object library and run the generator... +
+require_once('simpletest/unit_tester.php');
+require_once('simpletest/mock_objects.php');
+require_once('database_connection.php');
+
+Mock::generate('DatabaseConnection');
+
+ This generates a clone class called + MockDatabaseConnection. + We can now create instances of the new class within + our test case... +
+require_once('simpletest/unit_tester.php');
+require_once('simpletest/mock_objects.php');
+require_once('database_connection.php');
+
+Mock::generate('DatabaseConnection');
+
+class MyTestCase extends UnitTestCase {
+    
+    function testSomething() {
+        $connection = &new MockDatabaseConnection();
+    }
+}
+
+ Unlike the generated stubs the mock constructor needs a reference + to the test case so that it can dispatch passes and failures while + checking its expectations. + This means that mock objects can only be used within test cases. + Despite this their extra power means that stubs are hardly ever used + if mocks are available. +

+

+

Mocks as actors

+

+

+ The mock 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. +

+

+ We can also add extra methods to the mock when generating it + and choose our own class name... +

+Mock::generate('DatabaseConnection', 'MyMockDatabaseConnection', array('setOptions'));
+
+ Here the mock will behave as if the setOptions() + existed in the original class. + This is handy if a class has used the PHP overload() + mechanism to add dynamic methods. + You can create a special mock to simulate this situation. +

+

+ 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... +
+Mock::generate('Iterator');
+
+class IteratorTest extends UnitTestCase() {
+    
+    function testASequence() {
+        $iterator = &new MockIterator();
+        $iterator->setReturnValue('next', false);
+        $iterator->setReturnValueAt(0, 'next', 'First string');
+        $iterator->setReturnValueAt(1, 'next', 'Second string');
+        ...
+    }
+}
+
+ When next() is called on the + mock 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 mock 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 mocks have a filter system... +
+$config = &new MockConfiguration();
+$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 mock object 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 mock rather than a copy. + The PHP4 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 mock's + return list... +
+$thing = &new Thing();
+$vector = &new MockVector();
+$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. + This is compatible with PHP5 as well. +

+

+ These three factors, timing, parameters and whether to copy, + can be combined orthogonally. + For example... +

+$complex = &new MockComplexThing();
+$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 mocked using the mechanics above. +

+

+ Here's how... +

+Mock::generate('DatabaseConnection');
+Mock::generate('ResultIterator');
+
+class DatabaseTest extends UnitTestCase {
+    
+    function testUserFinder() {
+        $result = &new MockResultIterator();
+        $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 MockDatabaseConnection();
+        $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. +

+ +

Mocks as critics

+

+ Although the server stubs approach insulates your tests from + real world disruption, it is only half the benefit. + You can have the class under test receiving the required + messages, but is your new class sending correct ones? + Testing this can get messy without a mock objects library. +

+

+ By way of example, suppose we have a + SessionPool class that we + want to add logging to. + Rather than grow the original class into something more + complicated, we want to add this behaviour with a decorator (GOF). + The SessionPool code currently looks + like this... +

+class SessionPool {
+    function SessionPool() {
+        ...
+    }
+    
+    function &findSession($cookie) {
+        ...
+    }
+    ...
+}
+
+class Session {
+    ...
+}
+
+ While our logging code looks like this... +
+
+class Log {
+    function Log() {
+        ...
+    }
+    
+    function message() {
+        ...
+    }
+}
+
+class LoggingSessionPool {
+    function LoggingSessionPool(&$session_pool, &$log) {
+        ...
+    }
+    
+    function &findSession($cookie) {
+        ...
+    }
+    ...
+}
+
+ Out of all of this, the only class we want to test here + is the LoggingSessionPool. + In particular we would like to check that the + findSession() method is + called with the correct session ID in the cookie and that + it sent the message "Starting session $cookie" + to the logger. +

+

+ Despite the fact that we are testing only a few lines of + production code, here is what we would have to do in a + conventional test case: +

    +
  1. Create a log object.
  2. +
  3. Set a directory to place the log file.
  4. +
  5. Set the directory permissions so we can write the log.
  6. +
  7. Create a SessionPool object.
  8. +
  9. Hand start a session, which probably does lot's of things.
  10. +
  11. Invoke findSession().
  12. +
  13. Read the new Session ID (hope there is an accessor!).
  14. +
  15. Raise a test assertion to confirm that the ID matches the cookie.
  16. +
  17. Read the last line of the log file.
  18. +
  19. Pattern match out the extra logging timestamps, etc.
  20. +
  21. Assert that the session message is contained in the text.
  22. +
+ It is hardly surprising that developers hate writing tests + when they are this much drudgery. + To make things worse, every time the logging format changes or + the method of creating new sessions changes, we have to rewrite + parts of this test even though this test does not officially + test those parts of the system. + We are creating headaches for the writers of these other classes. +

+

+ Instead, here is the complete test method using mock object magic... +

+Mock::generate('Session');
+Mock::generate('SessionPool');
+Mock::generate('Log');
+
+class LoggingSessionPoolTest extends UnitTestCase {
+    ...
+    function testFindSessionLogging() {
+        $session = &new MockSession();
+        $pool = &new MockSessionPool();
+        $pool->setReturnReference('findSession', $session);
+        $pool->expectOnce('findSession', array('abc'));
+        
+        $log = &new MockLog();
+        $log->expectOnce('message', array('Starting session abc'));
+        
+        $logging_pool = &new LoggingSessionPool($pool, $log);
+        $this->assertReference($logging_pool->findSession('abc'), $session);
+    }
+}
+
+ We start by creating a dummy session. + We don't have to be too fussy about this as the check + for which session we want is done elsewhere. + We only need to check that it was the same one that came + from the session pool. +

+

+ findSession() is a factory + method the simulation of which is described above. + The point of departure comes with the first + expectOnce() call. + This line states that whenever + findSession() is invoked on the + mock, it will test the incoming arguments. + If it receives the single argument of a string "abc" + then a test pass is sent to the unit tester, otherwise a fail is + generated. + This was the part where we checked that the right session was asked for. + The argument list follows the same format as the one for setting + return values. + You can have wildcards and sequences and the order of + evaluation is the same. +

+

+ We use the same pattern to set up the mock logger. + We tell it that it should have + message() invoked + once only with the argument "Starting session abc". + By testing the calling arguments, rather than the logger output, + we insulate the test from any display changes in the logger. +

+

+ We start to run our tests when we create the new + LoggingSessionPool and feed + it our preset mock objects. + Everything is now under our control. +

+

+ This is still quite a bit of test code, but the code is very + strict. + If it still seems rather daunting there is a lot less of it + than if we tried this without mocks and this particular test, + interactions rather than output, is always more work to set + up. + More often you will be testing more complex situations without + needing this level or precision. + Also some of this can be refactored into a test case + setUp() method. +

+

+ Here is the full list of expectations you can set on a mock object + in SimpleTest... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExpectationNeeds tally() +
expect($method, $args)No
expectAt($timing, $method, $args)No
expectCallCount($method, $count)Yes
expectMaximumCallCount($method, $count)No
expectMinimumCallCount($method, $count)Yes
expectNever($method)No
expectOnce($method, $args)Yes
expectAtLeastOnce($method, $args)Yes
+ Where the parameters are... +

+
$method
+
The method name, as a string, to apply the condition to.
+
$args
+
+ The arguments as a list. Wildcards can be included in the same + manner as for setReturn(). + This argument is optional for expectOnce() + and expectAtLeastOnce(). +
+
$timing
+
+ The only point in time to test the condition. + The first call starts at zero. +
+
$count
+
The number of calls expected.
+
+ The method expectMaximumCallCount() + is slightly different in that it will only ever generate a failure. + It is silent if the limit is never reached. +

+

+ Also if you have juste one call in your test, make sure you're using + expectOnce.
+ Using $mocked->expectAt(0, 'method', 'args); + on its own will not be catched : + checking the arguments and the overall call count + are currently independant. +

+

+ Like the assertions within test cases, all of the expectations + can take a message override as an extra parameter. + Also the original failure message can be embedded in the output + as "%s". +

+ +

Other approaches

+

+ There are three approaches to creating mocks including the one + that SimpleTest employs. + Coding them by hand using a base class, generating them to + a file and dynamically generating them on the fly. +

+

+ Mock objects generated with SimpleTest + are dynamic. + They are created at run time in memory, using + eval(), rather than written + out to a file. + This makes the mocks easy to create, a one liner, + especially compared with hand + crafting them in a parallel class hierarchy. + The problem is that the behaviour is usually set up in the tests + themselves. + If the original objects change the mock versions + that the tests rely on can get out of sync. + This can happen with the parallel hierarchy approach as well, + but is far more quickly detected. +

+

+ The solution, of course, is to add some real integration + tests. + You don't need very many and the convenience gained + from the mocks more than outweighs the small amount of + extra testing. + You cannot trust code that was only tested with mocks. +

+

+ If you are still determined to build static libraries of mocks + because you want to simulate very specific behaviour, you can + achieve the same effect using the SimpleTest class generator. + In your library file, say mocks/connection.php for a + database connection, create a mock and inherit to override + special methods or add presets... +

+<?php
+    require_once('simpletest/mock_objects.php');
+    require_once('../classes/connection.php');
+
+    Mock::generate('Connection', 'BasicMockConnection');
+    class MockConnection extends BasicMockConnection {
+        function MockConnection() {
+            $this->BasicMockConnection();
+            $this->setReturn('query', false);
+        }
+    }
+?>
+
+ The generate call tells the class generator to create + a class called BasicMockConnection + rather than the usual MockConnection. + We then inherit from this to get our version of + MockConnection. + By intercepting in this way we can add behaviour, here setting + the default value of query() to be false. + By using the default name we make sure that the mock class + generator will not recreate a different one when invoked elsewhere in the + tests. + It never creates a class if it already exists. + As long as the above file is included first then all tests + that generated MockConnection should + now be using our one instead. + If we don't get the order right and the mock library + creates one first then the class creation will simply fail. +

+

+ Use this trick if you find you have a lot of common mock behaviour + or you are getting frequent integration problems at later + stages of testing. +

+ +
+ References and related information... + + + + + -- cgit v1.2.3