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