diff --git a/composer.json b/composer.json index 689a1a371..1f4983e48 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,8 @@ "autoload-dev": { "classmap": [ "tests/TestCase.php", - "tests/UiTestCase.php" + "tests/UiTestCase.php", + "tests/PluginTestCase.php" ] }, "scripts": { diff --git a/tests/PluginTestCase.php b/tests/PluginTestCase.php new file mode 100644 index 000000000..baaa85dd9 --- /dev/null +++ b/tests/PluginTestCase.php @@ -0,0 +1,193 @@ +make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + + $app['cache']->setDefaultDriver('array'); + $app->setLocale('en'); + + /* + * Store database in memory + */ + $app['config']->set('database.default', 'sqlite'); + $app['config']->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '' + ]); + + /* + * Modify the plugin path away from the test context + */ + $app->setPluginsPath(realpath(base_path().Config::get('cms.pluginsPath'))); + + return $app; + } + + /** + * Perform test case set up. + * @return void + */ + public function setUp() + { + /* + * Create application instance + */ + parent::setUp(); + + /* + * Rebind Laravel container in October Singletons + */ + UpdateManager::instance()->bindContainerObjects(); + PluginManager::instance()->bindContainerObjects(); + + /* + * Ensure system is up to date + */ + $this->runOctoberUpCommand(); + + /* + * Detect plugin from test and autoload it + */ + $this->pluginTestCaseLoadedPlugins = []; + $pluginCode = $this->guessPluginCodeFromTest(); + + if ($pluginCode !== false) { + $this->runPluginRefreshCommand($pluginCode, false); + } + + /* + * Disable mailer + */ + Mail::pretend(); + } + + /** + * Flush event listeners and collect garbage. + * @return void + */ + public function tearDown() + { + $this->flushModelEventListeners(); + parent::tearDown(); + unset($this->app); + } + + /** + * Migrate database using october:up command. + * @return void + */ + protected function runOctoberUpCommand() + { + Artisan::call('october:up'); + } + + /** + * Since the test environment has loaded all the test plugins + * natively, this method will ensure the desired plugin is + * loaded in the system before proceeding to migrate it. + * @return void + */ + protected function runPluginRefreshCommand($code, $throwException = true) + { + if (!preg_match('/^[\w+]*\.[\w+]*$/', $code)) { + if (!$throwException) return; + throw new Exception(sprintf('Invalid plugin code: "%s"', $code)); + } + + $manager = PluginManager::instance(); + $plugin = $manager->findByIdentifier($code); + + /* + * First time seeing this plugin, load it up + */ + if (!$plugin) { + $namespace = '\\'.str_replace('.', '\\', strtolower($code)); + $path = array_get($manager->getPluginNamespaces(), $namespace); + + if (!$path) { + if (!$throwException) return; + throw new Exception(sprintf('Unable to find plugin with code: "%s"', $code)); + } + + $plugin = $manager->loadPlugin($namespace, $path); + } + + /* + * Execute the command + */ + Artisan::call('plugin:refresh', ['name' => $code]); + + /* + * Spin over dependencies and refresh them too + */ + $this->pluginTestCaseLoadedPlugins[$code] = $plugin; + + if (!empty($plugin->require)) { + foreach ((array) $plugin->require as $dependency) { + + if (isset($this->pluginTestCaseLoadedPlugins[$code])) continue; + + $this->runPluginRefreshCommand($dependency); + } + } + } + + /** + * The models in October use a static property to store their events, these + * will need to be targeted and reset ready for a new test cycle. + * Pivot models are an exception since they are internally managed. + * @return void + */ + protected function flushModelEventListeners() + { + foreach (get_declared_classes() as $class) { + + if (!is_subclass_of($class, 'October\Rain\Database\Model')) continue; + if (is_subclass_of($class, 'October\Rain\Database\Pivot')) continue; + if ($class == 'October\Rain\Database\Pivot') continue; + + $class::flushEventListeners(); + } + + ActiveRecord::flushEventListeners(); + } + + /** + * Locates the plugin code based on the test file location. + * @return string|bool + */ + protected function guessPluginCodeFromTest() + { + $reflect = new ReflectionClass($this); + $path = $reflect->getFilename(); + $basePath = plugins_path(); + + $result = false; + + if (strpos($path, $basePath) === 0) { + $result = ltrim(str_replace('\\', '/', substr($path, strlen($basePath))), '/'); + $result = implode('.', array_slice(explode('/', $result), 0, 2)); + } + + return $result; + } +} \ No newline at end of file diff --git a/tests/README.md b/tests/README.md index fc5299f07..cde7a2c5e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,3 +1,54 @@ +# Plugin testing + +Plugins can be tested by creating a creating a file called `phpunit.xml` in the base directory with the following content, for example, in a file **/plugins/acme/blog/phpunit.xml**: + + + + + + ./tests + + + + + + + + + +Then a **tests/** directory can be created to contain the test classes. The file structure should mimic the base directory with classes having a `Test` suffix. Using a namespace for the class is also recommended. + + 'Hi!']); + $this->assertEquals(1, $post->id); + } + } + +The test class should extend the base class `PluginTestCase` and this is a special class that will set up the October database stored in memory, as part of the `setup()` method. It will also refresh the plugin being testing, along with any of the defined dependencies in the plugin registration file. This is the equivalent of running the following before each test: + + php artisan october:up + php artisan plugin:refresh Acme.Blog + +# System testing + ### Unit tests Unit tests can be performed by running `phpunit` in the root directory or inside `/tests/unit`.