Commit | Line | Data |
---|---|---|
b42b2bf9 H |
1 | <?php |
2 | // | |
3 | // PHP framework for testing, based on the design of "JUnit". | |
4 | // | |
5 | // Written by Fred Yankowski <fred@ontosys.com> | |
6 | // OntoSys, Inc <http://www.OntoSys.com> | |
7 | // | |
8 | // $Id: phpunit.php,v 1.1 2002/03/30 19:32:17 bmatzelle Exp $ | |
9 | ||
10 | // Copyright (c) 2000 Fred Yankowski | |
11 | ||
12 | // Permission is hereby granted, free of charge, to any person | |
13 | // obtaining a copy of this software and associated documentation | |
14 | // files (the "Software"), to deal in the Software without | |
15 | // restriction, including without limitation the rights to use, copy, | |
16 | // modify, merge, publish, distribute, sublicense, and/or sell copies | |
17 | // of the Software, and to permit persons to whom the Software is | |
18 | // furnished to do so, subject to the following conditions: | |
19 | // | |
20 | // The above copyright notice and this permission notice shall be | |
21 | // included in all copies or substantial portions of the Software. | |
22 | // | |
23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
24 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
25 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
26 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS | |
27 | // BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN | |
28 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
29 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
30 | // SOFTWARE. | |
31 | // | |
32 | error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE | | |
33 | E_CORE_ERROR | E_CORE_WARNING); | |
34 | ||
35 | /* | |
36 | interface Test { | |
37 | function run(&$aTestResult); | |
38 | function countTestCases(); | |
39 | } | |
40 | */ | |
41 | ||
42 | function trace($msg) { | |
43 | return; | |
44 | print($msg); | |
45 | flush(); | |
46 | } | |
47 | ||
48 | ||
49 | class Exception { | |
50 | /* Emulate a Java exception, sort of... */ | |
51 | var $message; | |
52 | function Exception($message) { | |
53 | $this->message = $message; | |
54 | } | |
55 | function getMessage() { | |
56 | return $this->message; | |
57 | } | |
58 | } | |
59 | ||
60 | class Assert { | |
61 | function assert($boolean, $message=0) { | |
62 | if (! $boolean) | |
63 | $this->fail($message); | |
64 | } | |
65 | ||
66 | function assertEquals($expected, $actual, $message=0) { | |
67 | if ($expected != $actual) { | |
68 | $this->failNotEquals($expected, $actual, "expected", $message); | |
69 | } | |
70 | } | |
71 | ||
72 | function assertRegexp($regexp, $actual, $message=false) { | |
73 | if (! preg_match($regexp, $actual)) { | |
74 | $this->failNotEquals($regexp, $actual, "pattern", $message); | |
75 | } | |
76 | } | |
77 | ||
78 | function failNotEquals($expected, $actual, $expected_label, $message=0) { | |
79 | // Private function for reporting failure to match. | |
80 | $str = $message ? ($message . ' ') : ''; | |
81 | $str .= "($expected_label/actual)<br>"; | |
82 | $htmlExpected = htmlspecialchars($expected); | |
83 | $htmlActual = htmlspecialchars($actual); | |
84 | $str .= sprintf("<pre>%s\n--------\n%s</pre>", | |
85 | $htmlExpected, $htmlActual); | |
86 | $this->fail($str); | |
87 | } | |
88 | } | |
89 | ||
90 | class TestCase extends Assert /* implements Test */ { | |
91 | /* Defines context for running tests. Specific context -- such as | |
92 | instance variables, global variables, global state -- is defined | |
93 | by creating a subclass that specializes the setUp() and | |
94 | tearDown() methods. A specific test is defined by a subclass | |
95 | that specializes the runTest() method. */ | |
96 | var $fName; | |
97 | var $fResult; | |
98 | var $fExceptions = array(); | |
99 | ||
100 | function TestCase($name) { | |
101 | $this->fName = $name; | |
102 | } | |
103 | ||
104 | function run($testResult=0) { | |
105 | /* Run this single test, by calling the run() method of the | |
106 | TestResult object which will in turn call the runBare() method | |
107 | of this object. That complication allows the TestResult object | |
108 | to do various kinds of progress reporting as it invokes each | |
109 | test. Create/obtain a TestResult object if none was passed in. | |
110 | Note that if a TestResult object was passed in, it must be by | |
111 | reference. */ | |
112 | if (! $testResult) | |
113 | $testResult = $this->_createResult(); | |
114 | $this->fResult = $testResult; | |
115 | $testResult->run(&$this); | |
116 | $this->fResult = 0; | |
117 | return $testResult; | |
118 | } | |
119 | ||
120 | function countTestCases() { | |
121 | return 1; | |
122 | } | |
123 | ||
124 | function runTest() { | |
125 | $name = $this->name(); | |
126 | // Since isset($this->$name) is false, no way to run defensive checks | |
127 | $this->$name(); | |
128 | } | |
129 | ||
130 | function setUp() /* expect override */ { | |
131 | //print("TestCase::setUp()<br>\n"); | |
132 | } | |
133 | ||
134 | function tearDown() /* possible override */ { | |
135 | //print("TestCase::tearDown()<br>\n"); | |
136 | } | |
137 | ||
138 | //////////////////////////////////////////////////////////////// | |
139 | ||
140 | ||
141 | function _createResult() /* protected */ { | |
142 | /* override this to use specialized subclass of TestResult */ | |
143 | return new TestResult; | |
144 | } | |
145 | ||
146 | function fail($message=0) { | |
147 | //printf("TestCase::fail(%s)<br>\n", ($message) ? $message : ''); | |
148 | /* JUnit throws AssertionFailedError here. We just record the | |
149 | failure and carry on */ | |
150 | $this->fExceptions[] = new Exception(&$message); | |
151 | } | |
152 | ||
153 | function error($message) { | |
154 | /* report error that requires correction in the test script | |
155 | itself, or (heaven forbid) in this testing infrastructure */ | |
156 | printf('<b>ERROR: ' . $message . '</b><br>'); | |
157 | $this->fResult->stop(); | |
158 | } | |
159 | ||
160 | function failed() { | |
161 | return count($this->fExceptions); | |
162 | } | |
163 | ||
164 | function getExceptions() { | |
165 | return $this->fExceptions; | |
166 | } | |
167 | ||
168 | function name() { | |
169 | return $this->fName; | |
170 | } | |
171 | ||
172 | function runBare() { | |
173 | $this->setup(); | |
174 | $this->runTest(); | |
175 | $this->tearDown(); | |
176 | } | |
177 | } | |
178 | ||
179 | ||
180 | class TestSuite /* implements Test */ { | |
181 | /* Compose a set of Tests (instances of TestCase or TestSuite), and | |
182 | run them all. */ | |
183 | var $fTests = array(); | |
184 | ||
185 | function TestSuite($classname=false) { | |
186 | if ($classname) { | |
187 | // Find all methods of the given class whose name starts with | |
188 | // "test" and add them to the test suite. We are just _barely_ | |
189 | // able to do this with PHP's limited introspection... Note | |
190 | // that PHP seems to store method names in lower case, and we | |
191 | // have to avoid the constructor function for the TestCase class | |
192 | // superclass. This will fail when $classname starts with | |
193 | // "Test" since that will have a constructor method that will | |
194 | // get matched below and then treated (incorrectly) as a test | |
195 | // method. So don't name any TestCase subclasses as "Test..."! | |
196 | if (floor(phpversion()) >= 4) { | |
197 | // PHP4 introspection, submitted by Dylan Kuhn | |
198 | $names = get_class_methods($classname); | |
199 | while (list($key, $method) = each($names)) { | |
200 | if (preg_match('/^test/', $method) && $method != "testcase") { | |
201 | $this->addTest(new $classname($method)); | |
202 | } | |
203 | } | |
204 | } | |
205 | else { | |
206 | $dummy = new $classname("dummy"); | |
207 | $names = (array) $dummy; | |
208 | while (list($key, $value) = each($names)) { | |
209 | $type = gettype($value); | |
210 | if ($type == "user function" && preg_match('/^test/', $key) | |
211 | && $key != "testcase") { | |
212 | $this->addTest(new $classname($key)); | |
213 | } | |
214 | } | |
215 | } | |
216 | } | |
217 | } | |
218 | ||
219 | function addTest($test) { | |
220 | /* Add TestCase or TestSuite to this TestSuite */ | |
221 | $this->fTests[] = $test; | |
222 | } | |
223 | ||
224 | function run(&$testResult) { | |
225 | /* Run all TestCases and TestSuites comprising this TestSuite, | |
226 | accumulating results in the given TestResult object. */ | |
227 | reset($this->fTests); | |
228 | while (list($na, $test) = each($this->fTests)) { | |
229 | if ($testResult->shouldStop()) | |
230 | break; | |
231 | $test->run(&$testResult); | |
232 | } | |
233 | } | |
234 | ||
235 | function countTestCases() { | |
236 | /* Number of TestCases comprising this TestSuite (including those | |
237 | in any constituent TestSuites) */ | |
238 | $count = 0; | |
239 | reset($fTests); | |
240 | while (list($na, $test_case) = each($this->fTests)) { | |
241 | $count += $test_case->countTestCases(); | |
242 | } | |
243 | return $count; | |
244 | } | |
245 | } | |
246 | ||
247 | ||
248 | class TestFailure { | |
249 | /* Record failure of a single TestCase, associating it with the | |
250 | exception(s) that occurred */ | |
251 | var $fFailedTestName; | |
252 | var $fExceptions; | |
253 | ||
254 | function TestFailure(&$test, &$exceptions) { | |
255 | $this->fFailedTestName = $test->name(); | |
256 | $this->fExceptions = $exceptions; | |
257 | } | |
258 | ||
259 | function getExceptions() { | |
260 | return $this->fExceptions; | |
261 | } | |
262 | function getTestName() { | |
263 | return $this->fFailedTestName; | |
264 | } | |
265 | } | |
266 | ||
267 | ||
268 | class TestResult { | |
269 | /* Collect the results of running a set of TestCases. */ | |
270 | var $fFailures = array(); | |
271 | var $fRunTests = 0; | |
272 | var $fStop = false; | |
273 | ||
274 | function TestResult() { } | |
275 | ||
276 | function _endTest($test) /* protected */ { | |
277 | /* specialize this for end-of-test action, such as progress | |
278 | reports */ | |
279 | } | |
280 | ||
281 | function getFailures() { | |
282 | return $this->fFailures; | |
283 | } | |
284 | ||
285 | function run($test) { | |
286 | /* Run a single TestCase in the context of this TestResult */ | |
287 | $this->_startTest($test); | |
288 | $this->fRunTests++; | |
289 | ||
290 | $test->runBare(); | |
291 | ||
292 | /* this is where JUnit would catch AssertionFailedError */ | |
293 | $exceptions = $test->getExceptions(); | |
294 | if ($exceptions) | |
295 | $this->fFailures[] = new TestFailure(&$test, &$exceptions); | |
296 | $this->_endTest($test); | |
297 | } | |
298 | ||
299 | function countTests() { | |
300 | return $this->fRunTests; | |
301 | } | |
302 | ||
303 | function shouldStop() { | |
304 | return $this->fStop; | |
305 | } | |
306 | ||
307 | function _startTest($test) /* protected */ { | |
308 | /* specialize this for start-of-test actions */ | |
309 | } | |
310 | ||
311 | function stop() { | |
312 | /* set indication that the test sequence should halt */ | |
313 | $fStop = true; | |
314 | } | |
315 | ||
316 | function countFailures() { | |
317 | return count($this->fFailures); | |
318 | } | |
319 | } | |
320 | ||
321 | ||
322 | class TextTestResult extends TestResult { | |
323 | /* Specialize TestResult to produce text/html report */ | |
324 | function TextTestResult() { | |
325 | $this->TestResult(); // call superclass constructor | |
326 | } | |
327 | ||
328 | function report() { | |
329 | /* report result of test run */ | |
330 | $nRun = $this->countTests(); | |
331 | $nFailures = $this->countFailures(); | |
332 | printf("<p>%s test%s run<br>", $nRun, ($nRun == 1) ? '' : 's'); | |
333 | printf("%s failure%s.<br>\n", $nFailures, ($nFailures == 1) ? '' : 's'); | |
334 | if ($nFailures == 0) | |
335 | return; | |
336 | ||
337 | print("<ol>\n"); | |
338 | $failures = $this->getFailures(); | |
339 | while (list($i, $failure) = each($failures)) { | |
340 | $failedTestName = $failure->getTestName(); | |
341 | printf("<li>%s\n", $failedTestName); | |
342 | ||
343 | $exceptions = $failure->getExceptions(); | |
344 | print("<ul>"); | |
345 | while (list($na, $exception) = each($exceptions)) | |
346 | printf("<li>%s\n", $exception->getMessage()); | |
347 | print("</ul>"); | |
348 | } | |
349 | print("</ol>\n"); | |
350 | } | |
351 | ||
352 | function _startTest($test) { | |
353 | printf("%s ", $test->name()); | |
354 | flush(); | |
355 | } | |
356 | ||
357 | function _endTest($test) { | |
358 | $outcome = $test->failed() | |
359 | ? "<font color=\"red\">FAIL</font>" | |
360 | : "<font color=\"green\">ok</font>"; | |
361 | printf("$outcome<br>\n"); | |
362 | flush(); | |
363 | } | |
364 | } | |
365 | ||
366 | ||
367 | class TestRunner { | |
368 | /* Run a suite of tests and report results. */ | |
369 | function run($suite) { | |
370 | $result = new TextTestResult; | |
371 | $suite->run($result); | |
372 | $result->report(); | |
373 | } | |
374 | } | |
375 | ||
376 | ?> |