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

vfs tests failing

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

I appreciate your input