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