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