17 July 2009

PHPUnit Custom Extension - Class ... could not be found in ...

(For the impatient, include your class early, that is the solution.)

So I have been using PHPUnit for quite some time now and find that it is a great library for testing. I also find that testing really helps in development. I also find that PHPUnit can be infuriating to use at times. For instance, I wanted to create a new base class that extendeds PHPUnit_Framework_TestCase so I can add some custom logging or something like that to all of my tests. So I simply create,
class lib_MyBaseUnitTest extends PHPUnit_Framework_TestCase {
    ....
}
Then I create my test cases,
class tests_MyFirstTest extends lib_MyBaseUnitTest {
    ....
}
Now whenever I run
phpunit tests/MyFirstTest.php
 I get the error:
Class lib_MyBaseUnitTest could not be found in tests/MyFirstTest.php
So clearly from the error the base class in not being loaded...? Actually, the problem is that the base class is being loaded by the autoloader when phpunit tries to load the test and the base class gets in the way. The phpunit code that causes the problem is located in StandardTestSuiteLoader.php.
PHPUnit_Util_Class::collectStart();
PHPUnit_Util_Fileloader::checkAndLoad($suiteClassFile, $syntaxCheck);
$loadedClasses = PHPUnit_Util_Class::collectEnd();
The collectStart call gathers the list of loaded classes and stores them in memory. The checkAndLoad call then includes each of the files listed in your suite, or the file specified on the commandline, thus loading more classes into memory. The collectEnd call now gathers the new list of loaded classes and diffs the result with the list collected in the collectStart call. Each of the resulting classes should be a test case that needs to be run. The problem is that the autoloader loaded both the specified test case and its base class. When PHPUnit tests to see if it can run the base class as a test, it can't and therefore throws and exception, "Class ... can not be found in ...".

So the solution in the end that I have taken is to place require_once calls in my bootstrap file for all custom base classes. This way the class will be in the initial list of classes created by the collectStart call. It will not be in the $loadedClasses variable and therefore will not cause an exception to be thrown.

2 comments:

Seth Fitzsimmons said...

Excellent explanation of the problem. Based on your discovery, I wrote up a patch that avoids having to require classes in your bootstrap file (we'll see if it comes through):

Index: PHPUnit/Runner/StandardTestSuiteLoader.php
===================================================================
--- PHPUnit/Runner/StandardTestSuiteLoader.php (revision 5126)
+++ PHPUnit/Runner/StandardTestSuiteLoader.php (working copy)
@@ -116,7 +116,7 @@
foreach ($loadedClasses as $loadedClass) {
$class = new ReflectionClass($loadedClass);

- if ($class->isSubclassOf($testCaseClass)) {
+ if ($class->isSubclassOf($testCaseClass) && !$class->isAbstract()) {
$suiteClassName = $loadedClass;
$testCaseClass = $loadedClass;


I sent it along to sb, but I can't link to a ticket on phpunit.de because I can't create one.

sebastian.bergmann said...

I cannot reproduce the issue with PHPUnit 3.3-SVN and PHPUnit 3.4-SVN.