1
votes

Question in short

How can I make the Autoloader find all classes required to run my PHP tests?

Question in detail

I want to autoload the classes that I am using in PHPUnit in Eclipse. My directory structure is as follows.

Project (called yii-app)
    protected
        dirA
            classA.php
        dirB
            classB.php
    yii-1.1.14.f0fee9
        Yii.php
    tests
        ClassATest.php
        ClassBTest.php
        bootstrap.php
        Autoloader.php

I use the bootstrap.php and Autoloader.php that I found here, see below for details. The Class classA does not make use of the Yii framework, and the tests in ClassATest run smoothly. The Class classB does make use of the Yii framework. One of the first lines is:

Yii::import('application.<path into some directory>.*')

When I try to run the tests in ClassBTest.php, I get the following error.

Fatal error: Class 'Yii' not found in /Users/physicalattraction/git/yii-app/protected/dirB/classB.php on line 3

Even if I register the entire project directory (including subdirectories), Class Yii is not found, while it is right there. What should I change to make these tests run as well?

Note

I have the same problem if I try to run the tests directly from the terminal, so it is not Eclipse related.

$ ./composer/vendor/bin/phpunit --bootstrap=tests/bootstrap.php tests
PHPUnit 4.5.1 by Sebastian Bergmann and contributors.

Fatal error: Class 'Yii' not found in /Users/physicalattraction/git/yii-app/protected/dirB/classB.php on line 3

Details

PHPUnit settings in Eclipse

PHPUnit settings in Eclipse

bootstrap.php

<?php
include_once('AutoLoader.php');
// Register the directory to your include files
Toolbox\Testing\AutoLoader::registerDirectory(__DIR__.'/../yii-1.1.14.f0fee9');
Toolbox\Testing\AutoLoader::registerDirectory(__DIR__.'/../protected');
?>

Autoloader.php

    <?php

namespace Toolbox\Testing;

/**
 * This class is an auto loader for use with vanilla PHP projects' testing environment. Use it in
 * the bootstrap to register classes without having to use a framework (which you can, and should if
 * it's a better solution for you) and without having to use includes everywhere.
 *
 * It assumes that the file path in relation to the namespace follows the PSR-0 standard.
 *
 * IMPORTANT NOTE: When just registering directories, the class has no ability to discern
 * conflicting class names in different namespaces, which means that classes with the same name will
 * override each other! Always use the registerNamespace()-method if possible!
 *
 * Inspired by Jess Telford's AutoLoader (http://jes.st/).
 *
 * @see http://jes.st/2011/phpunit-bootstrap-and-autoloading-classes/
 * @see http://petermoulding.com/php/psr
 * @see http://www.php-fig.org/psr/psr-0/
 *
 * @codeCoverageIgnore
 *
 * @category    Toolbox
 * @package     Testing
 *
 * @author      Helge Söderström <[email protected]>
 */

class AutoLoader {

    /**
     * An array keeping class names as key and their path as the value for classes registered with
     * AutoLoader::registerNamespace().
     *
     * @var array
     */
    protected static $namespaceClassNames = array();

    /**
     * An array keeping class names as key and their path as the value for classes registered with
     * AutoLoader::registerDirectory().
     *
     * @var array
     */
    protected static $directoryClassNames = array();


    /**
     * Store the filename (sans extension) & full path to all ".php" files found for a namespace.
     * The parameter should contain the root namespace as the key and the directory as a value.
     *
     * @param string $namespace
     * @param string $dirName
     * @return void
     */
    public static function registerNamespace($namespace, $dirName) {
        $directoryContents = new \DirectoryIterator($dirName);
        foreach($directoryContents as $file) {
            if ($file->isDir() && !$file->isLink() && !$file->isDot()) {
                $newNamespace = $namespace . "_" . $file->getFileName();
                $newDirName = $dirName . "/" . $file->getFilename();
                static::registerNamespace($newNamespace, $newDirName);
            } elseif (substr($file->getFilename(), -4) === '.php') {
                $className = substr($file->getFilename(), 0, -4);
                $namespacedClassName = $namespace . "_" . $className;
                $fileName = realpath($dirName) . "/" . $file->getFilename();
                static::$namespaceClassNames[$namespacedClassName] = $fileName;
            }
        }
    }


    /**
     * Store the filename (sans extension) & full path of all ".php" files found.
     *
     * NOTE: This method will not be able to differentiate the same class names in different
     *       namespaces and will therefore overwrite class names if multiple of the same name is
     *       found. If possible, use registerNamespace instead!
     *
     * @param string $dirName
     * @return void
     */
    public static function registerDirectory($dirName) {
        $directoryContents = new \DirectoryIterator($dirName);
        foreach ($directoryContents as $file) {
            if ($file->isDir() && !$file->isLink() && !$file->isDot()) {
                // Recurse into directories other than a few special ones.
                static::registerDirectory($file->getPathname());
            } elseif (substr($file->getFilename(), -4) === '.php') {
                // Save the class name / path of a .php file found.
                $className = substr($file->getFilename(), 0, -4);
                AutoLoader::registerClass($className, $file->getPathname());
            }
        }
    }


    /**
     * Caches a found class with the class name as key and its path as value for use when loading
     * on the fly. The class is registered with its class name only, no namespace.
     *
     * @param string $className
     * @param string $fileName
     * @return void
     */
    public static function registerClass($className, $fileName) {
        AutoLoader::$directoryClassNames[$className] = $fileName;
    }


    /**
     * Includes a found class in the runtime environment. Strips namespaces.
     *
     * @param string $className
     * @return void
     */
    public static function loadClass($className) {
        // First, see if we've registered the entire namespace.
        $namespacedClassName = str_replace('\\', '_', $className);
        if (isset(static::$namespaceClassNames[$namespacedClassName])) {
            require_once(static::$namespaceClassNames[$namespacedClassName]);
            return;
        }

        // Nope. Have we registered it as a directory?
        $psrDirectorySeparators = array('\\', '_');
        foreach($psrDirectorySeparators as $separator) {
            $separatorOccurrence = strrpos($className, $separator);
            if($separatorOccurrence !== false) {
                $className = substr($className, $separatorOccurrence + 1);
                break;
            }
        }

        if (isset(AutoLoader::$directoryClassNames[$className])) {
            require_once(AutoLoader::$directoryClassNames[$className]);
        }
    }

}

// Register our AutoLoad class as the system auto loader.
spl_autoload_register(array('Toolbox\Testing\AutoLoader', 'loadClass'));
?>
2

2 Answers

1
votes

The Autoloader probably doesn't find the YII class. Did you try adding:

Toolbox\Testing\AutoLoader::registerDirectory(DIR.'/../yii-1.1.14.f0fee9/framework');

to your bootstrap.php file. I think that the YII class is defined in the framework directory.

Another thing which you can try is to use the composer autoloader instead.

P.S It is a good practice to mirror your app directory/file structure in the tests directory. In your case ClassATest.php and ClassBTest.php should be separated into their own directories the same way they are separated in the protected directory.

0
votes

I found that with the following changes, it worked.

Have the following directory structure

project
    protected
        config
            main.php
            test.php
        controllers
        models
        tests
            fixtures
            functional
            report
            unit
            bootstrap.php
            phpunit.xml

In main.php: add the directories in which Yii looks for classes. Subdirectories are not automatically searched for by Yii, so you have to specify each directory individually here.

// autoloading model and component classes
'import' => array(
    'application.models.*',
    'application.models.support.*',
    'application.components.*',
    ....

Define the following test configuration in test.php.

<?php
return CMap::mergeArray(
    require(dirname(__FILE__).'/main.php'),
    array(
        'components'=>array(
            'fixture'=>array(
                'class'=>'system.test.CDbFixtureManager',
            ),
            'db'=>array(
                'class' => 'CDbConnection',
                'connectionString' => 'CONNECTIONSTRING',
                'emulatePrepare' => true,
                'username' => 'USERNAME',
                'password' => 'PASSWORD',
                'charset' => 'utf8',
            ),
        ),
    )
);
?>

Then, in the bootstrap file, only use the Yii Autoloader.

<?php
$yiit=__DIR__.'/../../yii-1.1.14.f0fee9/yiit.php';
$config=dirname(__FILE__).'/../config/test.php';
require_once($yiit);
// Include the following line if you want to write a WebTestCase
// require_once(dirname(__FILE__).'/WebTestCase.php');
Yii::createWebApplication($config);
?>

My test case needs a model of type Order, which is connected to a database table orders. Define a fixture in directory fixtures/Order.php.

<?php    
return array(
    'order1' => array(
        'id' => 1011,
        'order_number' => 'on_101'
    )
);
?>

Make the test case as indicated on the Yii and PHPUnit websites.

class MyExportTest extends CDbTestCase
{
    public $fixtures = array(
        'orders' => 'Order'
    );

    public function test_order()
    {
        $order = $this->orders('order1');
        $this->assertTrue($order instanceof Order);
        $r = $order->order_number;
        $e = "on_101";
        $this->assertEquals($r, $e,
        sprintf("Received order_number: \n%s\nExpected order_number: \n%s\n", $r, $e));
    }

    public function test_export()
    {
        $sut = new MyExport($tenantId, $userGroupId);
        $r = $sut->method_to_test()
        $e = "expected result";
        $this->assertEquals($r, $e,
        sprintf("Received result: \n%s\nExpected result: \n%s\n", $r, $e));
    }
}