New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CakePHP2] Support for running phpunit directly? #12700

Closed
mfn opened this Issue Nov 3, 2018 · 6 comments

Comments

Projects
None yet
6 participants
@mfn
Copy link
Contributor

mfn commented Nov 3, 2018

This is a (multiple allowed):

  • enhancement

  • feature-discussion (RFC)

  • CakePHP Version: 2.x

  • Platform and Target: Any

It's not possible to run phpunit directly because of how CakePHP2 integrates with it. E.g. running it leads to:

app $ Vendor/bin/phpunit Test/
PHP Fatal error:  Class 'CakeTestSuite' not found in /vagrant/project/app/Test/Case/AllComponentTest.php on line 3

Rather, you have to run ./cake test app …

But this means you can't integrate it with tooling like IDE/PhpStorm which expect to run phpunit directly and pass it appropriate flags.

This is very useful for re-running partial tests (i.e. only the failed tests), etc.

There's a 5 year old blog post by Jetbrains talking about creating a wrapper and I also found more recent versions of this script but none works with the current PhpStorm (or at least I couldn't make them work).

Rather then this PhpStorm specific workaround I'm interested or searching for interested parties getting this working with CakePHP2.

I understand it's quite a challenge regarding the whole bootstrapping and fixture process.

@dereuromark dereuromark added this to the 2.10.14 milestone Nov 3, 2018

@ADmad

This comment has been minimized.

Copy link
Member

ADmad commented Nov 3, 2018

As announced long ago CakePHP 2.x won't be receiving any new features. Hence closing the issue.

@ADmad ADmad closed this Nov 3, 2018

@tenkoma

This comment has been minimized.

Copy link
Member

tenkoma commented Dec 4, 2018

@mfn Hi.
I was thinking about the same thing.
With reference to Console/cake.php and ShellDispatchr, I prepared a bootstrap file for phpunit command and it worked, so I will write it down.
(re-running partial tests, run test with coverage, run test with debug)
I think that it is safe unless it is used only in local development and not used in CI build.

Note: When the phpunit command is executed, You may get a "class not defined" error. Apply App::uses() appropriately.

I saw this Issue and tried it, it was a lot easier to write a test, thank you. 😄

bootstrap and phpunit.xml.dist

Define constants and read Config before the test run.

app/Test/bootstrap_for_phpunit_command.php

<?php
/**
 * Bootstrap for phpunit command
 */
/**
 * copy from app/Console/cake.php
 */
$dispatcher = 'Cake' . DS . 'Console' . DS . 'ShellDispatcher.php';
$root = dirname(dirname(dirname(__FILE__)));
$appDir = basename(dirname(dirname(__FILE__)));
$install = $root . DS . 'lib';
$composerInstall = $root . DS . $appDir . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib';
// the following lines differ from its sibling
// /lib/Cake/Console/Templates/skel/Console/cake.php
if (file_exists($composerInstall . DS . $dispatcher)) {
    $install = $composerInstall;
}
ini_set('include_path', $install . PATH_SEPARATOR . ini_get('include_path'));
if (!include $dispatcher) {
    trigger_error('Could not locate CakePHP core files.', E_USER_ERROR);
}
unset($dispatcher);
define('ROOT', $root);
define('APP_DIR', $appDir);
define('APP', ROOT . DS . APP_DIR . DS);
// Use methods that initialize constants and environment variables, but the shell does not in ShellDispatcher class.
new ShellDispatcher(array(getenv('_'), '-working', $appDir));
unset($root, $appDir, $install, $composerInstall);

// Required for setup FixtureManager.
App::uses('AppFixtureManager', 'TestSuite/Fixture');

// A class that does not dare to call App::uses() because it calls it anywhere on the application. I will write it as an error
App::uses('ClassRegistry', 'Utility');

app/Test/bootstrap.php

<?php
// Read additional bootstrap.php when executed from phpunit command.
if (!defined('DS')) {
    define('DS', DIRECTORY_SEPARATOR);
    include_once dirname(__FILE__) . DS . 'bootstrap_for_phpunit_command.php';
}

phpunit.xml.dist

Please specify this file path by setting in PhpStorm.
(Preferences > Languages & Frameworks > PHP > Test Frameworks > (Any Setting) > Test Runner > Default Configuration File)

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    colors="true"
    bootstrap="./app/Test/bootstrap.php"
    >
    <filter>
        <whitelist>
            <directory suffix=".php">app/Config</directory>
            <directory suffix=".php">app/Console</directory>
            <directory suffix=".php">app/Controller</directory>
            <directory suffix=".php">app/Lib</directory>
            <directory suffix=".php">app/Model</directory>
            <directory suffix=".php">app/Routing</directory>
            <directory suffix=".php">app/TestSuite</directory>
            <directory suffix=".php">app/View</directory>
        </whitelist>
    </filter>
</phpunit>

Code to prepare FixtureManager instead of CakeTestRunner

app/TestSuite/AppControllerTestCase.php

<?php
App::uses('ControllerTestCase', 'TestSuite');
class AppControllerTestCase extends ControllerTestCase
{
    public function run(PHPUnit_Framework_TestResult $result = null)
    {
        $this->setUpFixtureManagerForPhpunitCommand();
        return parent::run($result);
    }

    /**
    * When executed from the phpunit command, since FixtureManager is not prepared, it can be prepared with TestCase::run()
    */
    private function setUpFixtureManagerForPhpunitCommand()
    {
        if (is_null($this->fixtureManager)) {
            App::uses('AppFixtureManager', 'TestSuite');
            if (class_exists('AppFixtureManager')) {
                $this->fixtureManager = new AppFixtureManager();
            } else {
                App::uses('CakeFixtureManager', 'TestSuite/Fixture');
                $this->fixtureManager = new CakeFixtureManager();
            }
            $this->fixtureManager->fixturize($this);
        }
    }
}

app/TestSuite/AppTestCase.php

<?php
App::uses('CakeTestCase', 'TestSuite');
class AppTestCase extends CakeTestCase
{
    public function run(PHPUnit_Framework_TestResult $result = null)
    {
        $this->setUpFixtureManagerForPhpunitCommand();
        return parent::run($result);
    }

    /**
    * When executed from the phpunit command, since FixtureManager is not prepared, it can be prepared with TestCase::run()
    */
    private function setUpFixtureManagerForPhpunitCommand()
    {
        if (is_null($this->fixtureManager)) {
            App::uses('AppFixtureManager', 'TestSuite');
            if (class_exists('AppFixtureManager')) {
                $this->fixtureManager = new AppFixtureManager();
            } else {
                App::uses('CakeFixtureManager', 'TestSuite/Fixture');
                $this->fixtureManager = new CakeFixtureManager();
            }
            $this->fixtureManager->fixturize($this);
        }
    }
}

app/TestSuite/Fixture/AppFixtureManager.php (Optional)

<?php
App::uses('CakeFixtureManager', 'TestSuite/Fixture');
class AppFixtureManager extends CakeFixtureManager
{
}
@mfn

This comment has been minimized.

Copy link
Contributor

mfn commented Dec 6, 2018

@tenkoma this is incredible!

I only had to to copy&paste your stuff and it literally worked on the first try!

Additional changes I did:

  • in phpunit.dist.xml I added explicitly testsuites so I can run app/Vendor/bin/phpunit without any args
        <testsuites>
            <testsuite name="Application Test Suite">
               <directory>./app/Test/</directory>
            </testsuite>
        </testsuites>
  • very custom: I added an autoloader which recursively scans app/ and can load un-namespaced class names from any folder (performance vs. DX trade-off), see below for code
  • I used the opportunity and upgraded to PHPUnit 7 (namespaced, function type declarations added)
    => works, but had to assimilate CakeTestCase and CakeFixtureManager to adapt some function signatures

The end result is fantastic, because now I can use PhpStorms PHPUnit integration and am now much faster iterating the tests (let alone debugging, which you outlined already).

Thank you very much!

Autoloader code

Append to bootstrap.php

spl_autoload_register(function (string $classNameToFind) {
    static $cakeClassMap = [];

    if (!$cakeClassMap) {
        $findPhpFilesRecursively = function (string $dir) use (&$cakeClassMap): void {
            $dirIter = new RecursiveDirectoryIterator($dir);
            $resursiveIter = new RecursiveIteratorIterator($dirIter);
            $regexIter = new RegexIterator($resursiveIter, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH);
            foreach ($regexIter as $file) {
                $file = $file[0];

                // Ignore `Vendor` files, they've their own resolution already
                if (false !== strpos($file, 'Vendor')) {
                    continue;
                }

                $className = str_replace('.php', '', basename($file));
                $cakeClassMap[$className] = $file;
            }
        };

        // Cake itself
        $findPhpFilesRecursively(ROOT . DIRECTORY_SEPARATOR . 'lib');
        // Project specific files; will overwrite clashes with CakePHP!
        $findPhpFilesRecursively(ROOT . DIRECTORY_SEPARATOR . APP_DIR);
    }

    if (isset($cakeClassMap[$classNameToFind])) {
        require_once $cakeClassMap[$classNameToFind];

        return null;
    }

    return false;
});
@kamilwylegala

This comment has been minimized.

Copy link

kamilwylegala commented Dec 11, 2018

@tenkoma @mfn

What version of PHP do you use?

I use similar setup with PHPUnit 6.* but I gave up using anything related to framework (ControllerTest case etc.) and I also use composer's autoloader for domain specific classes.

@mfn

This comment has been minimized.

Copy link
Contributor

mfn commented Dec 11, 2018

@kamilwylegala

What version of PHP do you use?

PHP 7.2

I use similar setup with PHPUnit 6.* but I gave up using anything related to framework (ControllerTest case etc.) and I also use composer's autoloader for domain specific classes.

Fair question: I don't have Controller tests and too use composers autoloader for domain specific classes.

Tried to write controller tests but they turned out too complex or simply not possible properly to mock certain parts so I moved the most stuff into domain classes or the models. Yay for fat models 👎 but turns our due to ClassRegistry::init() you get a test-ready version for it in tests which makes it a breeze to work with.

Mocking is then mostly done in the domain specific classes which can use better patterns (DI, for one).

PHPUnit 6.*

I got above setup by @tenkoma running in 5.* but immediately took the opportunity and upgraded to 7.* by basically copying CakeTestCase and CakeFixtureManager into the app/ and adapting parts (mostly minor function signature adaptions).

The specific project I'm working on is very old but we realized the test shortcomings years ago and thus changed the way we approach creating new code and basically moved off of everything too CakePHP specific 🤷‍♀️

@tenkoma

This comment has been minimized.

Copy link
Member

tenkoma commented Dec 16, 2018

@kamilwylegala

What version of PHP do you use?

I'm using PHP7.1 .
PHPUnit is 5.7, CakePHP is 2.10.
In my environment,I installed CakePHP using composer, I use composer autoload in library loading. However, Namespace is not declared in the class definition under the app/ directory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment