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);
}
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
.
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
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With