Setting up CakePHP 3 Plugin testing

2020-05-24 07:10发布

问题:

I've used bin/cake bake plugin PluginName to create a plugin. Part of it is that it creates phpunit.xml.dist, but bake doesn't create the folder structure or tests/bootstrap.php file that's required.

The Problem

I get a "No tests executed" message when I run phpunit:

$ phpunit
PHPUnit 5.1.3 by Sebastian Bergmann and contributors.

Time: 239 ms, Memory: 4.50Mb

No tests executed!

Background information

I've created my tests in my plugin folder under tests/TestCase. I don't think they are the issue, but I'll post them at the end.

I'm using the default phpunit.xml.dist file, and I'm using this for tests/bootstrap.php:

$findRoot = function ($root) {
    do {
        $lastRoot = $root;
        $root = dirname($root);
        if (is_dir($root . '/vendor/cakephp/cakephp')) {
            return $root;
        }
    } while ($root !== $lastRoot);
    throw new Exception("Cannot find the root of the application, unable to run tests");
};
$root = $findRoot(__FILE__);
unset($findRoot);
chdir($root);

define('ROOT', $root);
define('APP_DIR', 'App');
define('WEBROOT_DIR', 'webroot');
define('APP', ROOT . '/tests/App/');
define('CONFIG', ROOT . '/tests/config/');
define('WWW_ROOT', ROOT . DS . WEBROOT_DIR . DS);
define('TESTS', ROOT . DS . 'tests' . DS);
define('TMP', ROOT . DS . 'tmp' . DS);
define('LOGS', TMP . 'logs' . DS);
define('CACHE', TMP . 'cache' . DS);
define('CAKE_CORE_INCLUDE_PATH', ROOT . '/vendor/cakephp/cakephp');
define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS);
define('CAKE', CORE_PATH . 'src' . DS);

require ROOT . '/vendor/autoload.php';
require CORE_PATH . 'config/bootstrap.php';

The Unit Test

This lives in tests/TestCase/Color.php

<?php

namespace Contrast\TestCase;

use Cake\TestSuite\TestCase;
use Contrast\Text\Color;

/**
 * Contrast Text Color tests
 */
class TextColorTest extends TestCase
{
    /**
     * @test
     * @return void
     */
    public function testShouldHandleVarietyOfColors()
    {
        # Returns black
        $this->assertEquals(Color::getBlackWhiteContrast('#FFFFFF'), 'black');

        // Why won't you fail??
        $this->assertEquals(true, false);
    }

回答1:

First things first

The base namespace should be Contrast\Test, which is also what bake should have added to your applications composer.json files autoload and autoload-dev sections, as well as to the baked plugins composer.json file. In case you denied bake to edit yout composer.json file, you should add the autoload entry manually

"autoload": {
    "psr-4": {
        // ...
        "Contrast\\": "./plugins/Contrast/src"
    }
},
"autoload-dev": {
    "psr-4": {
        // ...
        "Contrast\\Test\\": "./plugins/Contrast/tests"
    }
},

and re-dump the autoloader

$ composer dump-autoload

So the namespace for your example test should be Contrast\Test\TestCase.

Also test files need to be postfixed with Test in order for PHPUnit to recognize them. And in order for the files to be autoloadable, you should stick to PSR-4, ie the files should have the same name as the class, ie your test file should be named TextColorTest.php, not Color.php.

Testing as a part of an application

When testing a plugin as a part on an application, there is not necessarily a need for a bootstrap file in the plugin tests (bake should, actually does generate one though), as you could run them with the configuration of your application, which has a test bootstrap file (tests/bootstrap.php) that includes your applications bootstrap file (config/bootstrap.php).

Consequently the tests would be run from your applications base folder, and you'd have to pass the plugin path, like

$ vendor/bin/phpunit plugins/Contrast

or you'd add an additional plugin testsuite in your apps main phpunit configuration file, where it says <!-- Add your plugin suites -->

<testsuite name="Contrast Test Suite">
    <directory>./plugins/Contrast/tests/TestCase</directory>
</testsuite>

That way the plugin tests will be run together with your app tests.

Finally, you can also run the tests from the plugin directory, given that a proper bootstrap file exists. The default one currently looks like:

<?php
/**
 * Test suite bootstrap for Contrast.
 *
 * This function is used to find the location of CakePHP whether CakePHP
 * has been installed as a dependency of the plugin, or the plugin is itself
 * installed as a dependency of an application.
 */
$findRoot = function ($root) {
    do {
        $lastRoot = $root;
        $root = dirname($root);
        if (is_dir($root . '/vendor/cakephp/cakephp')) {
            return $root;
        }
    } while ($root !== $lastRoot);

    throw new Exception("Cannot find the root of the application, unable to run tests");
};
$root = $findRoot(__FILE__);
unset($findRoot);

chdir($root);
require $root . '/config/bootstrap.php';

See also

  • Cookbook > Testing > Running Tests > Combining Test Suites for Plugins

  • Cookbook > Testing > Creating Tests for Plugins

Testing standalone developed plugins

When developing plugins in a standalone fashion, this is when you really need a bootstrap file that sets up the environment, and this is where the plugins composer.json file generated by bake is being used. By default the latter would look like

{
    "name": "your-name-here/Contrast",
    "description": "Contrast plugin for CakePHP",
    "type": "cakephp-plugin",
    "require": {
        "php": ">=5.4.16",
        "cakephp/cakephp": "~3.0"
    },
    "require-dev": {
        "phpunit/phpunit": "*"
    },
    "autoload": {
        "psr-4": {
            "Contrast\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Contrast\\Test\\": "tests",
            "Cake\\Test\\": "./vendor/cakephp/cakephp/tests"
        }
    }
}

The test bootstrap file generated when baking a plugin doesn't work out of the box, as all it would do would be trying to load the config/bootstrap.php file of your plugin, which by default doesn't even exist.

What the test bootstrap file needs to do depends on what the plugin is doing of course, but at the very least it should

  • define the basic constants and configuration that is used by the core
  • require the composer autoloader
  • require the CakePHP core bootstrap file
  • and load/register your plugin.

For example purposes, here's a tests/bootstrap.php example with a copy of what the current cakephp/app application template does, ie it basically configures a full application environment (with the application defined to be found in the tests/TestApp folder):

// from `config/paths.php`

if (!defined('DS')) {
    define('DS', DIRECTORY_SEPARATOR);
}
define('ROOT', dirname(__DIR__));
define('APP_DIR', 'test_app');
define('APP', ROOT . DS . 'tests' . DS . APP_DIR . DS);
define('CONFIG', ROOT . DS . 'config' . DS);
define('WWW_ROOT', APP . 'webroot' . DS);
define('TESTS', ROOT . DS . 'tests' . DS);
define('TMP', ROOT . DS . 'tmp' . DS);
define('LOGS', TMP . 'logs' . DS);
define('CACHE', TMP . 'cache' . DS);
define('CAKE_CORE_INCLUDE_PATH', ROOT . DS . 'vendor' . DS . 'cakephp' . DS . 'cakephp');
define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS);
define('CAKE', CORE_PATH . 'src' . DS);


// from `config/app.default.php` and `config/bootstrap.php`

use Cake\Cache\Cache;
use Cake\Console\ConsoleErrorHandler;
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Core\Configure\Engine\PhpConfig;
use Cake\Core\Plugin;
use Cake\Database\Type;
use Cake\Datasource\ConnectionManager;
use Cake\Error\ErrorHandler;
use Cake\Log\Log;
use Cake\Mailer\Email;
use Cake\Network\Request;
use Cake\Routing\DispatcherFactory;
use Cake\Utility\Inflector;
use Cake\Utility\Security;

require ROOT . DS . 'vendor' . DS . 'autoload.php';
require CORE_PATH . 'config' . DS . 'bootstrap.php';

$config = [
    'debug' => true,

    'App' => [
        'namespace' => 'App',
        'encoding' => env('APP_ENCODING', 'UTF-8'),
        'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
        'base' => false,
        'dir' => 'src',
        'webroot' => 'webroot',
        'wwwRoot' => WWW_ROOT,
        'fullBaseUrl' => false,
        'imageBaseUrl' => 'img/',
        'cssBaseUrl' => 'css/',
        'jsBaseUrl' => 'js/',
        'paths' => [
            'plugins' => [ROOT . DS . 'plugins' . DS],
            'templates' => [APP . 'Template' . DS],
            'locales' => [APP . 'Locale' . DS],
        ],
    ],

    'Asset' => [
        // 'timestamp' => true,
    ],

    'Security' => [
        'salt' => env('SECURITY_SALT', '__SALT__'),
    ],

    'Cache' => [
        'default' => [
            'className' => 'File',
            'path' => CACHE,
            'url' => env('CACHE_DEFAULT_URL', null),
        ],

        '_cake_core_' => [
            'className' => 'File',
            'prefix' => 'myapp_cake_core_',
            'path' => CACHE . 'persistent/',
            'serialize' => true,
            'duration' => '+2 minutes',
            'url' => env('CACHE_CAKECORE_URL', null),
        ],

        '_cake_model_' => [
            'className' => 'File',
            'prefix' => 'myapp_cake_model_',
            'path' => CACHE . 'models/',
            'serialize' => true,
            'duration' => '+2 minutes',
            'url' => env('CACHE_CAKEMODEL_URL', null),
        ],
    ],

    'Error' => [
        'errorLevel' => E_ALL & ~E_DEPRECATED,
        'exceptionRenderer' => 'Cake\Error\ExceptionRenderer',
        'skipLog' => [],
        'log' => true,
        'trace' => true,
    ],

    'EmailTransport' => [
        'default' => [
            'className' => 'Mail',
            // The following keys are used in SMTP transports
            'host' => 'localhost',
            'port' => 25,
            'timeout' => 30,
            'username' => 'user',
            'password' => 'secret',
            'client' => null,
            'tls' => null,
            'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null),
        ],
    ],

    'Email' => [
        'default' => [
            'transport' => 'default',
            'from' => 'you@localhost',
            //'charset' => 'utf-8',
            //'headerCharset' => 'utf-8',
        ],
    ],

    'Datasources' => [
        'test' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            //'port' => 'non_standard_port_number',
            'username' => 'my_app',
            'password' => 'secret',
            'database' => 'test_myapp',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
            'quoteIdentifiers' => false,
            'log' => false,
            //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
            'url' => env('DATABASE_TEST_URL', null),
        ],
    ],

    'Log' => [
        'debug' => [
            'className' => 'Cake\Log\Engine\FileLog',
            'path' => LOGS,
            'file' => 'debug',
            'levels' => ['notice', 'info', 'debug'],
            'url' => env('LOG_DEBUG_URL', null),
        ],
        'error' => [
            'className' => 'Cake\Log\Engine\FileLog',
            'path' => LOGS,
            'file' => 'error',
            'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
            'url' => env('LOG_ERROR_URL', null),
        ],
    ],

    'Session' => [
        'defaults' => 'php',
    ],
];
Configure::write($config);

date_default_timezone_set('UTC');
mb_internal_encoding(Configure::read('App.encoding'));
ini_set('intl.default_locale', Configure::read('App.defaultLocale'));

Cache::config(Configure::consume('Cache'));
ConnectionManager::config(Configure::consume('Datasources'));
Email::configTransport(Configure::consume('EmailTransport'));
Email::config(Configure::consume('Email'));
Log::config(Configure::consume('Log'));
Security::salt(Configure::consume('Security.salt'));

DispatcherFactory::add('Asset');
DispatcherFactory::add('Routing');
DispatcherFactory::add('ControllerFactory');

Type::build('time')
    ->useImmutable()
    ->useLocaleParser();
Type::build('date')
    ->useImmutable()
    ->useLocaleParser();
Type::build('datetime')
    ->useImmutable()
    ->useLocaleParser();


// finally load/register the plugin using a custom path

Plugin::load('Contrast', ['path' => ROOT]);

In order for classes to be autoloadable from the test application folder, you'd have to add a corresponding autoload entry in your composer.json file (and again re-dump the autoloader), like:

    "autoload-dev": {
        "psr-4": {
            "Contrast\\Test\\": "tests",
            "Contrast\\TestApp\\": "tests/test_app/src", // < here we go
            "Cake\\Test\\": "./vendor/cakephp/cakephp/tests"
        }
    }

So now with the plugin been created using bake, the environment configured and the CakePHP and PHPUnit dependencies installed, you should be able to run your tests as you would with an application, ie

$ vendor/bin/phpunit