Testing and developing a Codeception module 02
October 13, 2015
What's to inject and what's an argument in a Codeception module development.
Configuration and injection
Once I've found the possibility to inject dependencies in the module constructor method it's time to write some tests.
Since the module works and relies on WordPress methods to work I'm using function-mocker for my stubbing and mocking needs; it's aliases to the Test
name.
One of the tasks the module will carry out is activating a list of specified plugins, in WordPress terms this means calling the activate_{$plugin}
action for each listed plugin; using function mocker that's an easy test to write
/**
* @test
* it should call activate action if one plugin to activate
*/
public function it_should_call_activate_action_if_one_plugin_to_activate()
{
// `make_container` is a wrapper around
// \Codeception\Util\Stub::make('Codeception\Lib\ModuleContainer');
$pluginSlug = 'some-plugin/some-file.php';
$config = ['activatePlugins' => $pluginSlug];
$sut = new EmbeddedWP(make_container(), $config);
$do_action = Test::replace('do_action');
$sut->activatePlugins();
$do_action->wasCalledWithOnce(["activate_{$pluginSlug}"]);
}
This is a case where the dependency injection happens in the configuration object at it makes perfect sense; another test to make sure the user I will be able to specify multiple plugins to activate and will be free to use the plugin-not-in-a-folder.php
format work on the same principle
/**
* @test
* it should not do activation action if no plugins to activate
*/
public function it_should_not_do_activation_action_if_no_plugins_to_activate()
{
$sut = new EmbeddedWP(make_container(), []);
$do_action = Test::replace('do_action');
$sut->activatePlugins();
$do_action->wasNotCalled();
}
/**
* @test
* it should call activate action once for each plugin to activate
*/
public function it_should_call_activate_action_once_for_each_plugin_to_activate()
{
$plugins = ['plugin-a/plugin-a.php',
'plugin-b/plugin-b.php',
'plugin-c/plugin-c.php'];
$sut = new EmbeddedWP(make_container(), ['activatePlugins' => $plugins]);
$do_action = Test::replace('do_action');
$sut->activatePlugins();
$do_action->wasCalledTimes(count($plugins));
foreach ($plugins as $plugin) {
$do_action->wasCalledWithOnce(['activate_' . $plugin]);
}
}
/**
* @test
* it should allow the user to activate single file plugins
*/
public function it_should_allow_the_user_to_activate_single_file_plugins()
{
$pluginSlug = 'some-file.php';
$config = ['activatePlugins' => $pluginSlug];
$sut = new EmbeddedWP(make_container(), $config);
$do_action = Test::replace('do_action');
$sut->activatePlugins();
$do_action->wasCalledWithOnce(["activate_{$pluginSlug}"]);
}
public function weirdPluginBasenames()
{
return [
['some-file.js'],
['some-file'],
['some-folder/some-file'],
['some-folder/some-file.js'],
['rails.rb']
];
}
/**
* @test
* it should throw for weird plugin basenames in plugins to activate
* @dataProvider weirdPluginBasenames
*/
public function it_should_throw_for_weird_plugin_basenames_in_plugins_to_activate($weirdPluginBasenamej)
{
$config = ['activatePlugins' => $weirdPluginBasenamej];
$sut = new EmbeddedWP(make_container(), $config);
$this->expectConfigException();
$sut->activatePlugins();
}
Dependency or configuration parameter?
A later tests raises an interesting question: the desired behaviour is for the module to "cast" the main plugin file to a WordPress standard plugin basename in the format plugin-folder/plugin-file.php
using the root folder of the project to do so; if I'm developing a plugin using the wp-embedded module and have the following file structure
/my-plugin-dev-folder
|
my-plugin.php
/src
/tests
/vendor
composer.json
codeception.yml
...
and the following configuration for the EmbeddedWP
module in the Codeception configuration file (codeception.yml
):
modules:
config:
EmbeddedWP:
mainFile: my-plugin.php
activatePlugins:
- my-plugin.php
then the module should call the do_action
function with the activate_my-plugin-dev-folder/my-plugin.php
parameter.
This to allow the plugin in development to call the register_activation_hook
properly
register_activation_hook(__FILE__, 'my_plugin_activation');
A first approach could be to allow for the injection of the project root in the constructor method of the module like this
public function __construct(ModuleContainer $moduleContainer, $config = null, $projectRoot = null)
{
parent::__construct($moduleContainer, $config);
$this->projectRoot = $projectRoot ? PathUtils::untrailslashit($projectRoot) : codecept_root_dir();
}
and leverage the injection possibility in tests
/**
* @test
* it should cast main plugin file to folder and plugin file format
*/
public function it_should_cast_main_plugin_file_to_folder_and_plugin_file_format()
{
$projectRoot = __DIR__;
$config = ['activatePlugins' => 'some-plugin.php', 'mainFile' => 'some-plugin.php'];
$sut = new EmbeddedWP(make_container(), $config, $projectRoot);
$do_action = Test::replace('do_action');
$sut->activatePlugins();
$do_action->wasCalledWithOnce(['activate_' . basename(__DIR__) . '/some-plugin.php']);
}
or allow the user to specify a projectRoot
setting in the configuration file and, implicitly, allow its injection
/**
* @test
* it should cast main plugin file to folder and plugin file format
*/
public function it_should_cast_main_plugin_file_to_folder_and_plugin_file_format()
{
$config = [
'activatePlugins' => 'some-plugin.php',
'mainFile' => 'some-plugin.php',
'projectRoot' => __DIR__];
$sut = new EmbeddedWP(make_container(), $config);
$do_action = Test::replace('do_action');
$sut->activatePlugins();
$do_action->wasCalledWithOnce(['activate_' . basename(__DIR__) . '/some-plugin.php']);
}
I'd like to avoid settings overload and cannot see a case where the user might need to define a location for the plugin under test different from Codeception root folder and will hence opt for the first option.
Since the point of dependency injection is mocking the dependency itself I will take the injection a step further and provide the EmbededWP
constructor with a class instance in place of a string and modify it accordingly.
After some more test rewriting I've added another dependency to the constructor to be able to mock and stub filesystem operation calls and be able, in this way, to control any input and output of the module
public function __construct(ModuleContainer $moduleContainer, $config = null, PathFinder $pathFinder = null, \Symfony\Component\Filesystem\Filesystem $filesystem = null)
{
parent::__construct($moduleContainer, $config);
$this->pathFinder = $pathFinder ?: new Paths(codecept_root_dir());
$this->filesystem = $filesystem ?: new \Symfony\Component\Filesystem\Filesystem();
}
The tad\EmbeddedWp\Paths
class is nothing complex and takes charge of finding the embedded WordPress installation components around the module internal file and folder structure, the \Symfony\Component\Filesystem\Filesystem
class is a wrapper around file system operations; this two dependencies allow me to rewrite the test above
/**
* @test
* it should cast main plugin file to folder and plugin file format
*/
public function it_should_cast_main_plugin_file_to_folder_and_plugin_file_format()
{
$projectRoot = __DIR__;
$pathFinder = Test::replace('tad\EmbeddedWp\PathFinder')->method('getRootDir', $projectRoot)->get();
$config = ['activatePlugins' => 'some-plugin.php',
'mainFile' => 'some-plugin.php'];
$sut = new EmbeddedWP(make_container(), $config, $pathFinder);
$do_action = Test::replace('do_action');
$sut->activatePlugins();
$do_action->wasCalledWithOnce(['activate_' . basename(__DIR__) . '/some-plugin.php']);
}
and one more that will require stubbing both dependencies
/**
* @test
* it should allow for required plugin path to be relative to project root
*/
public function it_should_allow_for_required_plugin_path_to_be_relative_to_project_root()
{
$projectRoot = VfsStream::url('project_dir');
$pathFinder = Test::replace('tad\EmbeddedWP\PathFinder')
->method('getRootDir', $projectRoot)
->method('getWpRootFolder', $projectRoot . '/includes/embedded-wordpress')
->get();
$filesystem = Test::replace('Symfony\Component\Filesystem\Filesystem')->method('symlink')->get();
$sut = new EmbeddedWP(make_container(), ['requiredPlugins' => 'plugins/some-plugin/some-file.php'], $pathFinder, $filesystem);
$sut->loadRequiredPlugins();
$filesystem->wasCalledOnce('symlink');
}
Next
I will go over filesystem simulation that's just hinted in code using the VfsStream package and move on with the module testing.