Testing and developing a Codeception module 03
October 14, 2015
Working with the filesystem.
Not a Codeception module exclusive
Dealing with the filesystem is not a Codeception only feature by any chance but it's usually an hairy concept to abstract and mock that's too easily resolved creating files and folders for real.
I will raise my hand in guilty admission for having done it but took the chance offered by wp-embedded refactoring to "make it right".
Virtual File System
The usual drill would be to create the files and folders needed for testing in a known and accessible location to remove them after each test, this is not a bad system per se but a cumbersome and error prone one: some failing tests might leave files and folders behind, permissions might be difficult to simulate and file and folder creation is not a good partner for continuous integration.
Now that I've done my homework in exposing the "why" it's time to expose the "how".
VfsStream
My choice of a virtual file system to use in tests falls on the VfsStream package so it's time to pull it into the project as development requirement using Composer
composer require --dev mikey179/vfsStream
and start with a test dealing with files.
Required plugins
The Embedded WP module allows the user to define a list of plugins that are required before the plugin or theme in development can run; I might be developing a plugin that requires a "Plugin A" plugin to work.
Since the module packs a self-contained WordPress installation to do its job there must be a way for the user to tell where the required plugin files are located either outside of the plugin or theme under test folder or inside it.
Covering the first scenario I might have a cloned version of the plugin in the /Users/Me/cloned-plugins/plugin-a
folder and the EmbeddedWP
module configuration would be
modules:
config:
EmbeddedWp:
mainFile: my-plugin.php
requiredPlugins:
- /Users/Me/cloned-plugins/plugin-a/plugin-a.php
I will write a first test to make sure the user is allowed to use that format and that the required plugin is symbolically linked in the embedded WordPress installation plugins folder.
As a first step I will mock a filesystem structure in the set up method of the tests and make sure to have a bare bones simulation of a realistic folder tree.
When setting up the file structure arrays are folders while key to string couples will be files and their contents; the final call to the VfsStream::setup
method will make sure the virtual file system stream is created and accessible.
protected function _before()
{
// FunctionMocker set up
Test::setUp();
// the folder structure
$structure = [
'Users' => [
'Me' => [
'cloned-plugins' => [
'plugin-a' => [
'plugin-a.php' => '<?php //plugin-a'
]
]
]
],
'my-plugin' => [
'vendor' => [
'required-plugins' => ['plugin-b' => ['plugin-b.php' => '<?php // plugin-b']]
],
'lucatume' => [
'wp-embedded' => [
'src' => [
'embedded-wordpress' => [
// EmbeddedWp will look up this file to make sure this is a valid WP install
'wp-settings.php' => '<?php // wp-settings.php'
]
]
]
]
]
];
// create the stream
VfsStream::setup('folder_tree', null, $structure);
}
In the tests I will access the virtual filesystem using the VfsStream::url('folder_tree')
method that sounds like this in plain english
Give me the URL to the root of the virtual file system with an id of 'folder_tree'.
In PHP terms the vfs://
stream is as good as the file://
one when it comes to file operations; PHP will fall back to the file system stream when a stream scheme is not defined but file_*
functions will work on streams as well.
I've added inline comments in code to make the steps more comprehensible:
/**
* @test
* it should allow for required plugins to be specified as abspath
*/
public function it_should_allow_for_required_plugins_to_be_specified_as_abspath()
{
// will be 'vfs://folder_tree'
$root = VfsStream::url('folder_tree');
$pluginABasename = 'plugin-a/plugin-a.php';
// will be 'vfs://folder_tree/Users/Me/cloned-plugins/plugin-a/plugin-a.php'
$clonedPluginPath = $root . '/Users/Me/cloned-plugins/' . $pluginABasename;
// will be 'vfs://folder_tree/my-plugin/vendor/lucatume/wp-embedded/src/embedded-wordpress'
$embeddedWpPath = $root . '/my-plugin/vendor/lucatume/wp-embedded/src/embedded-wordpress';
// Set up the EmbeddedWP path provider to return paths in the virtual file system
$pathFinder = new Paths($root, $embeddedWpPath);
// spy on the filesystem operations to check that the symlink is happening, uses FunctionMocker
$filesystem = Test::replace('Symfony\Component\Filesystem\Filesystem')->method('symlink')->get();
// finally instantiate the injected module
$config = ['requiredPlugins' => $clonedPluginPath];
$sut = new EmbeddedWP(make_container(), $config, $pathFinder, $filesystem);
// execute the method under test
$sut->loadRequiredPlugins();
// check the 'symlink' function was called with the expected parameters
$destination = dirname($embeddedWpPath . '/wp-content/plugins/' . $pluginABasename);
$from = dirname($clonedPluginPath);
$filesystem->wasCalledWithOnce([
$from,
$destination
], 'symlink');
}
on the same note another test makes sure I will be able to require plugins that are stored in the project itself, e.g. in the vendor
folder
/**
* @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('folder_tree') . '/my-plugin';
$embeddedWpPath = $projectRoot . '/my-plugin/vendor/lucatume/wp-embedded/src/embedded-wordpress';
$pathFinder = new Paths($projectRoot, $embeddedWpPath);
$filesystem = Test::replace('Symfony\Component\Filesystem\Filesystem')->method('symlink')->get();
$pluginRelativePath = 'vendor/required-plugins/plugin-b/plugin-b.php';
$sut = new EmbeddedWP(make_container(), ['requiredPlugins' => $pluginRelativePath], $pathFinder, $filesystem);
$sut->loadRequiredPlugins();
$from = dirname($projectRoot . '/' . $pluginRelativePath);
$destination = $embeddedWpPath . '/wp-content/plugins/plugin-b';
$filesystem->wasCalledWithOnce([$from, $destination], 'symlink');
}
A gotcha
I might need to define multiple virtual file systems and do this in my test set up method
protected function _before()
{
$structureOne = [
'folderOne' => [
'fileOne' => 'File one contents', 'subFolderOne' => ['fileTwo' => 'File two contents']
]
];
$structureTwo = [
'folderTwo' => [
'fileTwo' => 'File two contents', 'subFolderTwo' => ['fileTwo' => 'File two contents']
]
];
$structureThree = [
'folderThree' => [
'fileThree' => 'File three contents', 'subFolderThree' => ['fileThree' => 'File three contents']
]
];
VfsStream::setup('one', null, $structureOne);
VfsStream::setup('two', null, $structureTwo);
VfsStream::setup('three', null, $structureThree);
}
but the test below will fail at the second check
/**
* Allows for multiple vfs
*/
public function test_allows_for_multiple_vfs()
{
// pass
$this->assertFileExists(vfsStream::url('three') . '/folderThree/fileThree');
// fail
$this->assertFileExists(vfsStream::url('two') . '/folderTwo/fileTwo');
// not reached
$this->assertFileExists(vfsStream::url('one') . '/folderOne/fileOne');
}
[](http://theaveragedev.local/wordpress/wp-content/uploads/2015/10/2015-10-14-at-16.10.png)
While more than one virtual file system can be defined the vfsStream wrapper needs to be explicitly told which one is the current one; the rewritten tests below will pass
protected function _before()
{
$structureOne = [
'folderOne' => [
'fileOne' => 'File one contents', 'subFolderOne' => ['fileTwo' => 'File two contents']
]
];
$structureTwo = [
'folderTwo' => [
'fileTwo' => 'File two contents', 'subFolderTwo' => ['fileTwo' => 'File two contents']
]
];
$structureThree = [
'folderThree' => [
'fileThree' => 'File three contents', 'subFolderThree' => ['fileThree' => 'File three contents']
]
];
vfsStreamWrapper::register();
$this->one = vfsStream::newDirectory('one');
$this->two = vfsStream::newDirectory('two');
$this->three = vfsStream::newDirectory('three');
vfsStream::create($structureOne, $this->one);
vfsStream::create($structureTwo, $this->two);
vfsStream::create($structureThree, $this->three);
}
/**
* Allows for multiple vfs
*/
public function test_allows_for_multiple_vfs()
{
vfsStreamWrapper::setRoot($this->three);
$this->assertFileExists(vfsStream::url('three/folderThree/fileThree'));
vfsStreamWrapper::setRoot($this->two);
$this->assertFileExists(vfsStream::url('two/folderTwo/fileTwo'));
vfsStreamWrapper::setRoot($this->one);
$this->assertFileExists(vfsStream::url('one/folderOne/fileOne'));
vfsStreamWrapper::setRoot($this->three);
$this->assertFileExists(vfsStream::url('three/folderThree/fileThree'));
}
I've come to think of it as a "track change".