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