Testing and developing a Codeception module 03

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) · GitHub”) 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

Required plugins

The Embedded WP) · GitHub”) 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
}

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');
}

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”.