+
+More control over mock objects
+
+
+ The default behaviour of the + mock objects + in + SimpleTest + is either an identical match on the argument or to allow any argument at all. + For almost all tests this is sufficient. + Sometimes, though, you want to weaken a test case. +
++ One place where a test can be too tightly coupled is with + text matching. + Suppose we have a component that outputs a helpful error + message when something goes wrong. + You want to test that the correct error was sent, but the actual + text may be rather long. + If you test for the text exactly, then every time the exact wording + of the message changes, you will have to go back and edit the test suite. +
++ For example, suppose we have a news service that has failed + to connect to its remote source. +
+class NewsService {
+ ...
+ function publish(&$writer) {
+ if (! $this->isConnected()) {
+ $writer->write('Cannot connect to news service "' .
+ $this->_name . '" at this time. ' .
+ 'Please try again later.');
+ }
+ ...
+ }
+}
+
+ Here it is sending its content to a
+ Writer class.
+ We could test this behaviour with a
+ MockWriter like so...
+
+class TestOfNewsService extends UnitTestCase {
+ ...
+ function testConnectionFailure() {
+ $writer = &new MockWriter($this);
+ $writer->expectOnce('write', array(
+ 'Cannot connect to news service ' .
+ '"BBC News" at this time. ' .
+ 'Please try again later.'));
+
+ $service = &new NewsService('BBC News');
+ $service->publish($writer);
+
+ $writer->tally();
+ }
+}
+
+ This is a good example of a brittle test.
+ If we decide to add additional instructions, such as
+ suggesting an alternative news source, we will break
+ our tests even though no underlying functionality
+ has been altered.
+
+ + To get around this, we would like to do a regular expression + test rather than an exact match. + We can actually do this with... +
+class TestOfNewsService extends UnitTestCase {
+ ...
+ function testConnectionFailure() {
+ $writer = &new MockWriter($this);
+ $writer->expectOnce(
+ 'write',
+ array(new WantedPatternExpectation('/cannot connect/i')));
+
+ $service = &new NewsService('BBC News');
+ $service->publish($writer);
+
+ $writer->tally();
+ }
+}
+
+ Instead of passing in the expected parameter to the
+ MockWriter we pass an
+ expectation class called
+ WantedPatternExpectation.
+ The mock object is smart enough to recognise this as special
+ and to treat it differently.
+ Rather than simply comparing the incoming argument to this
+ object, it uses the expectation object itself to
+ perform the test.
+
+ + The WantedPatternExpectation takes + the regular expression to match in its constructor. + Whenever a comparison is made by the MockWriter + against this expectation class, it will do a + preg_match() with this pattern. + With our test case above, as long as "cannot connect" + appears in the text of the string, the mock will issue a pass + to the unit tester. + The rest of the text does not matter. +
++ The possible expectation classes are... +
| EqualExpectation | An equality, rather than the stronger identity comparison | +
| NotEqualExpectation | An inequality comparison | +
| IndenticalExpectation | The default mock object check which must match exactly | +
| NotIndenticalExpectation | Inverts the mock object logic | +
| WantedPatternExpectation | Uses a Perl Regex to match a string | +
| NoUnwantedPatternExpectation | Passes only if failing a Perl Regex | +
| IsAExpectation | Checks the type or class name only | +
| NotAExpectation | Opposite of the IsAExpectation | +
| MethodExistsExpectation | Checks a method is available on an object | +
+
+Using expectations to control stubs
+
+
+ The expectation classes can be used not just for sending assertions + from mock objects, but also for selecting behaviour for either + the + mock objects + or the + server stubs. + Anywhere a list of arguments is given, a list of expectation objects + can be inserted instead. +
++ Suppose we want an authorisation server stub to simulate a successful login + only if it receives a valid session object. + We can do this as follows... +
+Stub::generate('Authorisation');
+
+$authorisation = new StubAuthorisation();
+$authorisation->setReturnValue(
+ 'isAllowed',
+ true,
+ array(new IsAExpectation('Session', 'Must be a session')));
+$authorisation->setReturnValue('isAllowed', false);
+
+ We have set the default stub behaviour to return false when
+ isAllowed is called.
+ When we call the method with a single parameter that
+ is a Session object, it will return true.
+ We have also added a second parameter as a message.
+ This will be displayed as part of the mock object
+ failure message if this expectation is the cause of
+ a failure.
+
+ + This kind of sophistication is rarely useful, but is included for + completeness. +
+ +
+
+Creating your own expectations
+
+
+ The expectation classes have a very simple structure. + So simple that it is easy to create your own versions for + commonly used test logic. +
++ As an example here is the creation of a class to test for + valid IP addresses. + In order to work correctly with the stubs and mocks the new + expectation class should extend + SimpleExpectation... +
+class ValidIp extends SimpleExpectation {
+
+ function test($ip) {
+ return (ip2long($ip) != -1);
+ }
+
+ function testMessage($ip) {
+ return "Address [$ip] should be a valid IP address";
+ }
+}
+
+ There are only two methods to implement.
+ The test() method should
+ evaluate to true if the expectation is to pass, and
+ false otherwise.
+ The testMessage() method
+ should simply return some helpful text explaining the test
+ that was carried out.
+
+ + This class can now be used in place of the earlier expectation + classes. +
+ +
+
+Under the bonnet of the unit tester
+
+
+ The SimpleTest unit testing framework + also uses the expectation classes internally for the + UnitTestCase class. + We can also take advantage of these mechanisms to reuse our + homebrew expectation classes within the test suites directly. +
++ The most crude way of doing this is to use the + SimpleTest::assertExpectation() method to + test against it directly... +
+class TestOfNetworking extends UnitTestCase {
+ ...
+ function testGetValidIp() {
+ $server = &new Server();
+ $this->assertExpectation(
+ new ValidIp(),
+ $server->getIp(),
+ 'Server IP address->%s');
+ }
+}
+
+ This is a little untidy compared with our usual
+ assert...() syntax.
+
+ + For such a simple case we would normally create a + separate assertion method on our test case rather + than bother using the expectation class. + If we pretend that our expectation is a little more + complicated for a moment, so that we want to reuse it, + we get... +
+class TestOfNetworking extends UnitTestCase {
+ ...
+ function assertValidIp($ip, $message = '%s') {
+ $this->assertExpectation(new ValidIp(), $ip, $message);
+ }
+
+ function testGetValidIp() {
+ $server = &new Server();
+ $this->assertValidIp(
+ $server->getIp(),
+ 'Server IP address->%s');
+ }
+}
+
+ It is unlikely we would ever need this degree of control
+ over the testing machinery.
+ It is rare to need the expectations for more than pattern
+ matching.
+ Also, complex expectation classes could make the tests
+ harder to read and debug.
+ These mechanisms are really of most use to authors of systems
+ that will extend the test framework to create their own tool set.
+
+
+