/trunk/docs/source/en/mock_objects_tutorial.xml
XML | 360 lines | 342 code | 17 blank | 1 comment | 0 complexity | 1e08b5bae5f0c9a9587f52b56066b92d MD5 | raw file
Possible License(s): LGPL-2.1
- <?xml version="1.0"?>
- <!-- $Id: mock_objects_tutorial.xml 1979 2010-03-15 11:16:16Z lastcraft $ -->
- <page title="Mock Objects" here="Using mock objects">
- <long_title>PHP unit testing tutorial - Using mock objects in PHP</long_title>
- <content>
- <section name="refactor" title="Refactoring the tests again">
- <p>
- Before more functionality is added there is some refactoring
- to do.
- We are going to do some timing tests and so the
- <code>TimeTestCase</code> class definitely needs
- its own file.
- Let's say <em>tests/time_test_case.php</em>...
- <php><![CDATA[
- <strong><?php
- require_once('simpletest/unit_tester.php');
- abstract class TimeTestCase extends UnitTestCase {
- function assertSameTime($time1, $time2, $message = '') {
- if (! $message) {
- $message = "Time [$time1] should match time [$time2]";
- }
- $this->assertTrue(
- ($time1 == $time2) || ($time1 + 1 == $time2),
- $message);
- }
- }
- ?></strong>
- ]]></php>
- We can then <code>require()</code> this file into
- the <em>all_tests.php</em> script.
- </p>
- </section>
- <section name="timestamp" title="Adding a timestamp to the Log">
- <p>
- I don't know quite what the format of the log message should
- be for the test, so to check for a timestamp we could do the
- simplest possible thing, which is to look for a sequence of digits.
- <php><![CDATA[
- <?php
- require_once('simpletest/autorun.php');<strong>
- require_once('time_test_case.php');</strong>
- require_once('../classes/log.php');<strong>
- require_once('../classes/clock.php');
- class TestOfLogging extends TimeTestCase {</strong>
- function setUp() {
- @unlink('../temp/test.log');
- }
- function tearDown() {
- @unlink('../temp/test.log');
- }
- function getFileLine($filename, $index) {
- $messages = file($filename);
- return $messages[$index];
- }
- function testCreatingNewFile() { ... }
- function testAppendingToFile() { ... }
- <strong>
- function testTimestampIsEmittedInMessage() {
- $log = new Log('../temp/test.log');
- $log->message('Test line');
- $this->assertTrue(
- preg_match('/(\d+)/', $this->getFileLine('../temp/test.log', 0), $matches),
- 'Found timestamp');
- $clock = new clock();
- $this->assertSameTime((integer)$matches[1], $clock->now(), 'Correct time');
- }</strong>
- }
- ?>
- ]]></php>
- The test case creates a new <code>Log</code> object and writes a message.
- We look for a digit sequence and then test it against the current
- time using our <code>Clock</code> object.
- Of course it doesn't work until we write the code.
- <div class="demo">
- <h1>All tests</h1>
- <span class="pass">Pass</span>: log_test.php->Log class test->testAppendingToFile->Expecting [/Test line 1/] in [Test line 1]<br />
- <span class="pass">Pass</span>: log_test.php->Log class test->testAppendingToFile->Expecting [/Test line 2/] in [Test line 2]<br />
- <span class="pass">Pass</span>: log_test.php->Log class test->testCreatingNewFile->Created before message<br />
- <span class="pass">Pass</span>: log_test.php->Log class test->testCreatingNewFile->File created<br />
- <span class="fail">Fail</span>: log_test.php->Log class test->testTimestampIsEmittedInMessage->Found timestamp<br />
- <br />
- <b>Notice</b>: Undefined offset: 1 in <b>/home/marcus/projects/lastcraft/tutorial_tests/tests/log_test.php</b> on line <b>44</b><br />
- <span class="fail">Fail</span>: log_test.php->Log class test->testTimestampIsEmittedInMessage->Correct time<br />
- <span class="pass">Pass</span>: clock_test.php->Clock class test->testClockAdvance->Advancement<br />
- <span class="pass">Pass</span>: clock_test.php->Clock class test->testClockTellsTime->Now is the right time<br />
- <div style="padding: 8px; margin-top: 1em; background-color: red; color: white;">3/3 test cases complete.
- <strong>6</strong> passes, <strong>2</strong> fails and <strong>2</strong> exceptions.</div>
- </div>
- The test suite is still showing the passes from our earlier
- modification.
- </p>
- <p>
- We can get the tests to pass simply by adding a timestamp
- when writing out to the file.
- Yes, of course all of this is trivial and
- I would not normally test this fanatically, but it is going
- to illustrate a more general problem.
- The <em>log.php</em> file becomes...
- <php><![CDATA[
- <?php<strong>
- require_once('../classes/clock.php');</strong>
- class Log {
- private $path;
- function __construct($path) {
- $this->path = $path;
- }
- function message($message) {
- <strong>$clock = new Clock();</strong>
- $file = fopen($this->path, 'a');
- <strong>fwrite($file, "[" . $clock->now() . "] $message\n");</strong>
- fclose($file);
- }
- }
- ?>
- ]]></php>
- The tests should now pass.
- </p>
- <p>
- Our new test is full of problems, though.
- What if our time format changes to something else?
- Things are going to be a lot more complicated to test if this
- happens.
- It also means that any changes to the clock class time
- format will cause our logging tests to fail also.
- This means that our log tests are tangled up with the clock tests
- and extremely fragile.
- It lacks cohesion, which is the same as saying it is not
- tightly focused, testing facets of the clock as well as the log.
- </p>
- <p>
- Our problems are caused in part because the clock output
- is unpredictable, yet all we really want to test is that the
- logging message contains the output of <code>Clock::now()</code>.
- We don't really care about the contents of that method call.
- </p>
- <p>
- From the testing point of view, everything we've just done is
- wrong.
- We've failed to control the inputs and the outputs of the object
- we are testing.
- </p>
- <p>
- Can we make that call predictable?
- We could if we could get the log to use a dummy version
- of the clock for the duration of the test.
- The dummy clock class would have to behave the same way
- as the <code>Clock</code> class
- except for the fixed output from the
- <code>now()</code> method.
- Hey, that would even free us from using the
- <code>TimeTestCase</code> class!
- </p>
- <p>
- We could write such a class pretty easily although it is
- rather tedious work.
- We just create another clock class with same interface
- except that the <code>now()</code> method
- returns a value that we can change with some other setter method.
- That is quite a lot of work for a pretty minor test.
- </p>
- <p>
- Except that it is hardly any work at all.
- </p>
- </section>
- <section name="mock" title="A mock clock">
- <p>
- To reach instant testing clock nirvana we need
- only three extra lines of code...
- <php><![CDATA[
- require_once('simpletest/mock_objects.php');
- ]]></php>
- This includes the mock generator code.
- It is simplest to place this in the <em>all_tests.php</em>
- script as it gets used rather a lot.
- <php><![CDATA[
- Mock::generate('Clock');
- ]]></php>
- This is the line that does the work.
- The code generator scans the class for all of its
- methods, creates code to generate an identically
- interfaced or inherited class, but with the name
- mangled to have "Mock" added.
- It then <code>eval()</code>s the new code to
- create the new class.
- <php><![CDATA[
- $clock = new MockClock();
- ]]></php>
- This line can be added to any test method we are interested in.
- It creates the dummy clock ready to receive our instructions.
- </p>
- <p>
- Our test case is on the first steps of a radical clean up.
- First is to actually use the <code>MockClock</code> in our test...
- <php><![CDATA[
- <?php
- require_once('simpletest/autorun.php');<strong>
- require_once('simpletest/mock_objects.php');</strong>
- require_once('../classes/log.php');
- require_once('../classes/clock.php');<strong>
- Mock::generate('Clock');</strong>
- class TestOfLogging extends <strong>UnitTestCase</strong> {
- function setUp() {
- @unlink('../temp/test.log');
- }
- function tearDown() {
- @unlink('../temp/test.log');
- }
- function getFileLine($filename, $index) {
- $messages = file($filename);
- return $messages[$index];
- }
- function testCreatingNewFile() { ... }
- function testAppendingToFile() { ... }
- function testTimestamps() {
- <strong>$clock = new MockClock();</strong>
- <strong>$clock->returns('now', 'Timestamp');</strong>
- $log = new Log('../temp/test.log');
- $log->message('Test line', $clock);
- $this->assertPattern(
- <strong>'/Timestamp/'</strong>,
- $this->getFileLine('../temp/test.log', 0));
- }
- }
- ?>
- ]]></php>
- Note that we don't need our <code>TimeTestCase</code> anymore.
- It will still be useful for testing the <code>Clock</code>, but we
- don't need it for testing the <code>Loc</code> anymore.
- </p>
- <p>
- This test method creates a <code>MockClock</code>
- object and then sets the return value of the
- <code>now()</code> method to be the string
- "Timestamp".
- Every time we call <code>$clock->now()</code>
- it will return this string.
- This should be easy to spot.
- </p>
- <p>
- Next we create our log and send a message.
- We pass into the <code>message()</code>
- call the clock we would like to use.
- This means that we will have to add an optional parameter to
- the logging class to make testing possible...
- <php><![CDATA[
- class Log {
- private $path;
- function Log($path) {
- $this->path = $path;
- }
- function message($message, <strong>$clock = false</strong>) {<strong>
- $clock = $clock? $clock : new Clock();</strong>
- $file = fopen($this->path, 'a');
- fwrite($file, "[" . $clock->now() . "] $message\n");
- fclose($file);
- }
- }
- ]]></php>
- All of the tests now pass and they test only the logging code.
- We can breathe easy again.
- </p>
- <p>
- Does that extra parameter in the <code>Log</code>
- class bother you?
- We have changed the interface just to facilitate testing after
- all.
- Are not interfaces the most important thing?
- Have we sullied our class with test code?
- </p>
- <p>
- It's not ideal, but consider this.
- Next chance you get, look at a circuit board, perhaps the motherboard
- of the computer you are looking at right now.
- On most boards you will find the odd empty hole, or solder
- joint with nothing attached or perhaps a pin or socket
- that has no obvious function.
- Chances are that some of these are for expansion and
- variations, but most of the remainder will be for testing.
- </p>
- <p>
- The factories making the boards many times over wasting material
- on parts that do not add to the final function.
- If hardware engineers can make this sacrifice of elegance I am
- sure we can too.
- Our sacrifice wastes no materials after all.
- </p>
- <p>
- Still bother you?
- Actually it bothers me too.
- If it really bothers you, then move the creation of the clock
- into another protected factory method.
- Then subclass the clock for testing and override the
- factory method with one that returns the mock.
- Your tests are clumsier, but your interface is intact.
- </p>
- <p>
- I leave the decision to you, but note that we have some
- automation in the next section that helps make the subclassing
- easier.
- </p>
- </section>
- </content>
- <internal>
- <link>
- <a href="#refactor">Refactoring the tests</a> so we can reuse
- our new time test.
- </link>
- <link>Adding <a href="#timestamp">Log timestamps</a>.</link>
- <link><a href="#mock">Mocking the clock</a> to make the test cohesive.</link>
- </internal>
- <external>
- <link>
- This follows the <a href="first_test_tutorial.php">unit test tutorial</a>.
- </link>
- <link>
- Next is distilling <a href="boundary_classes_tutorial.php">boundary classes</a>.
- </link>
- <link>
- You will need the <a href="simple_test.php">SimpleTest</a>
- tool to run the examples.
- </link>
- <link>
- <a href="http://www.mockobjects.com/">Mock objects</a> papers.
- </link>
- </external>
- <meta>
- <keywords>
- software development,
- php programming,
- programming php,
- software development tools,
- php tutorial,
- free php scripts,
- architecture,
- php resources,
- mock objects,
- junit,
- php testing,
- unit test,
- php testing
- </keywords>
- </meta>
- </page>