+ The following assumes that you are familiar with the concept + of unit testing as well as the PHP web development language. + It is a guide for the impatient new user of + SimpleTest. + For fuller documentation, especially if you are new + to unit testing see the ongoing + documentation, and for + example test cases see the + unit testing tutorial. +
+ +
+
+Using the tester quickly
+
+
+ Amongst software testing tools, a unit tester is the one + closest to the developer. + In the context of agile development the test code sits right + next to the source code as both are written simultaneously. + In this context SimpleTest aims to be a complete PHP developer + test solution and is called "Simple" because it + should be easy to use and extend. + It wasn't a good choice of name really. + It includes all of the typical functions you would expect from + JUnit and the + PHPUnit + ports, but also adds + mock objects. + It has some JWebUnit + functionality as well. + This includes web page navigation, cookie testing and form submission. +
++ The quickest way to demonstrate is with an example. +
++ Let us suppose we are testing a simple file logging class called + Log in classes/log.php. + We start by creating a test script which we will call + tests/log_test.php and populate it as follows... +
+<?php +require_once('simpletest/unit_tester.php'); +require_once('simpletest/reporter.php'); +require_once('../classes/log.php'); +?> ++ Here the simpletest folder is either local or in the path. + You would have to edit these locations depending on where you + placed the toolset. + Next we create a test case... +
+<?php +require_once('simpletest/unit_tester.php'); +require_once('simpletest/reporter.php'); +require_once('../classes/log.php'); + +class TestOfLogging extends UnitTestCase { +} +?> ++ Now we have five lines of scaffolding code and still no tests. + However from this part on we get return on our investment very quickly. + We'll assume that the Log class + takes the file name to write to in the constructor and we have + a temporary folder in which to place this file... +
+<?php +require_once('simpletest/unit_tester.php'); +require_once('simpletest/reporter.php'); +require_once('../classes/log.php'); + +class TestOfLogging extends UnitTestCase { + + function testCreatingNewFile() { + @unlink('/temp/test.log'); + $log = new Log('/temp/test.log'); + $this->assertFalse(file_exists('/temp/test.log')); + $log->message('Should write this to a file'); + $this->assertTrue(file_exists('/temp/test.log')); + } +} +?> ++ When a test case runs it will search for any method that + starts with the string test + and execute that method. + We would normally have more than one test method of course. + Assertions within the test methods trigger messages to the + test framework which displays the result immediately. + This immediate response is important, not just in the event + of the code causing a crash, but also so that + print statements can display + their content right next to the test case concerned. + +
+ To see these results we have to actually run the tests. + If this is the only test case we wish to run we can achieve + it with... +
+<?php +require_once('simpletest/unit_tester.php'); +require_once('simpletest/reporter.php'); +require_once('../classes/log.php'); + +class TestOfLogging extends UnitTestCase { + + function testCreatingNewFile() { + @unlink('/temp/test.log'); + $log = new Log('/temp/test.log'); + $this->assertFalse(file_exists('/temp/test.log')); + $log->message('Should write this to a file'); + $this->assertTrue(file_exists('/temp/test.log')); + } +} + +$test = &new TestOfLogging(); +$test->run(new HtmlReporter()); +?> ++ +
+ On failure the display looks like this... +
testoflogging
+ Fail: testcreatingnewfile->True assertion failed.+
testoflogging
++<?php +class Log { + + function Log($file_path) { + } + + function message() { + } +} +?>; ++ + + +
+ It is unlikely in a real application that we will only ever run + one test case. + This means that we need a way of grouping cases into a test + script that can, if need be, run every test in the application. +
++ Our first step is to strip the includes and to undo our + previous hack... +
+<?php +require_once('../classes/log.php'); + +class TestOfLogging extends UnitTestCase { + + function testCreatingNewFile() { + @unlink('/temp/test.log'); + $log = new Log('/temp/test.log'); + $this->assertFalse(file_exists('/temp/test.log')); + $log->message('Should write this to a file'); + $this->assertTrue(file_exists('/temp/test.log')); + } +} +?> ++ Next we create a new file called tests/all_tests.php + and insert the following code... +
+<?php +require_once('simpletest/unit_tester.php'); +require_once('simpletest/reporter.php'); + +$test = &new GroupTest('All tests'); +$test->addTestFile('log_test.php'); +$test->run(new HtmlReporter()); +?> ++ The method GroupTest::addTestFile() + will include the test case file and read any new classes created + that are descended from SimpleTestCase, of which + UnitTestCase is one example. + Just the class names are stored for now, so that the test runner + can instantiate the class when it works its way + through your test suite. + +
+ For this to work properly the test case file should not blindly include + any other test case extensions that do not actually run tests. + This could result in extra test cases being counted during the test + run. + Hardly a major problem, but to avoid this inconvenience simply add + a SimpleTestOptions::ignore() directive + somewhere in the test case file. + Also the test case file should not have been included + elsewhere or no cases will be added to this group test. + This would be a more serious error as if the test case classes are + already loaded by PHP the GroupTest::addTestFile() + method will not detect them. +
++ To display the results it is necessary only to invoke + tests/all_tests.php from the web server. +
+ + ++ Let's move further into the future. +
++ Assume that our logging class is tested and completed. + Assume also that we are testing another class that is + required to write log messages, say a + SessionPool. + We want to test a method that will probably end up looking + like this... +
+ +class SessionPool { + ... + function logIn($username) { + ... + $this->_log->message("User $username logged in."); + ... + } + ... +} + ++ In the spirit of reuse we are using our + Log class. + A conventional test case might look like this... +
+ +<?php +require_once('../classes/log.php'); +require_once('../classes/session_pool.php'); + +class TestOfSessionLogging extends UnitTestCase { + + function setUp() { + @unlink('/temp/test.log'); + } + + function tearDown() { + @unlink('/temp/test.log'); + } + + function testLogInIsLogged() { + $log = new Log('/temp/test.log'); + $session_pool = &new SessionPool($log); + $session_pool->logIn('fred'); + $messages = file('/temp/test.log'); + $this->assertEqual($messages[0], "User fred logged in.\n"); + } +} +?> ++ This test case design is not all bad, but it could be improved. + We are spending time fiddling with log files which are + not part of our test. Worse, we have created close ties + with the Log class and + this test. + What if we don't use files any more, but use ths + syslog library instead? + Did you notice the extra carriage return in the message? + Was that added by the logger? + What if it also added a time stamp or other data? + +
+ The only part that we really want to test is that a particular + message was sent to the logger. + We reduce coupling if we can pass in a fake logging class + that simply records the message calls for testing, but + takes no action. + It would have to look exactly like our original though. +
++ If the fake object doesn't write to a file then we save on deleting + the file before and after each test. We could save even more + test code if the fake object would kindly run the assertion for us. +
+
+ Too good to be true? + Luckily we can create such an object easily... ++<?php +require_once('../classes/log.php'); +require_once('../classes/session_pool.php'); +Mock::generate('Log'); + +class TestOfSessionLogging extends UnitTestCase { + + function testLogInIsLogged() { + $log = &new MockLog($this); + $log->expectOnce('message', array('User fred logged in.')); + $session_pool = &new SessionPool($log); + $session_pool->logIn('fred'); + $log->tally(); + } +} +?> ++ The tally() call is needed to + tell the mock object that time is up for the expected call + count. + Without it the mock would wait forever for the method + call to come in without ever actually notifying the test case. + The other test will be triggered when the call to + message() is invoked on the + MockLog object. + The mock call will trigger a parameter comparison and then send the + resulting pass or fail event to the test display. + Wildcards can be included here too so as to prevent tests + becoming too specific. + +
+ The mock objects in the SimpleTest suite can have arbitrary + return values set, sequences of returns, return values + selected according to the incoming arguments, sequences of + parameter expectations and limits on the number of times + a method is to be invoked. +
++ For this test to run the mock objects library must have been + included in the test suite, say in all_tests.php. +
+ + ++ One of the requirements of web sites is that they produce web + pages. + If you are building a project top-down and you want to fully + integrate testing along the way then you will want a way of + automatically navigating a site and examining output for + correctness. + This is the job of a web tester. +
++ The web testing in SimpleTest is fairly primitive, there is + no JavaScript for example. + To give an idea here is a trivial example where a home + page is fetched, from which we navigate to an "about" + page and then test some client determined content. +
+<?php +require_once('simpletest/web_tester.php'); +require_once('simpletest/reporter.php'); + +class TestOfAbout extends WebTestCase { + + function setUp() { + $this->get('http://test-server/index.php'); + $this->clickLink('About'); + } + + function testSearchEngineOptimisations() { + $this->assertTitle('A long title about us for search engines'); + $this->assertWantedPattern('/a popular keyphrase/i'); + } +} +$test = &new TestOfAbout(); +$test->run(new HtmlReporter()); +?> ++ With this code as an acceptance test you can ensure that + the content always meets the specifications of both the + developers and the other project stakeholders. + + + +