Testing with the StreamWrapper File System Libraries

I was working on Bldr, the nice task runner tool to automate development. Some days ago I ran into a problem. Usually when you work with bldr-io, it has you create at least several files on the project root directory. We have the bldr.json to specify the external dependencies.

For instance in my case I want to use gush-bldr-block so I have:

{
    "require": {
        "bldr-io/gush-block": "dev-master@dev"
    },
    "config": {
        "vendor-dir": "app/build/vendor",
        "block-loader": "app/build/blocks.yml"
    }
}

Before I couldn’t have pointed to block-loader successfully. It was not finding the blocks that it downloaded under the app/build/vendor folder. Feel confused? Let me rewind. Bldr has composer built-in. This means that you run bldr install to read the bldr.json and install additional dependencies to the composer.json native project dependencies. Bldr will use your composer.json and in addition the bldr.json to create its autoload. The idea is that in its autoload it will load blocks or third party packages that will be used to build your project.

Why would you need to add more dependencies to your project? Because packages to build projects can be reused. Common tasks for maintenance and others can be packaged in this way. Now if we build a package that you don’t like but it is included with bldr it will be bloated unnecessarily. By doing this then bldr allows for customization of the same tool. Some other tools choose extensions however you have to plug their dependencies into the composer.json. Bldr’s embedded composer splits these dependencies and yet ensures they are compatible or compatible with your project packages other requirements.

imports:
    - { resource: app/build/profiles.yml }
    - { resource: app/build/tasks.yml }

bldr:
    name: vendor/project
    description: some description

Bldr needs files like bldr.yml, .bldr folder that contains tasks.yml and profiles.yml if you do import these from bldr.yml. In addition it will generate a bldr.lock because of the bldr.json. That is a lot of files. Since this is configuration related, I think it is better to store them under app/build say on a project with an app folder that stores mostly configuration.

So Instead of having all these files spread out into the root of the project we move them there and it looks very clean.

Screenshot 2014-09-22 02.32.17
You can keep creating your custom blocks and rerun bldr install.

So we saw the need of a PR, now let’s look at how I wrote the PR and the test for it. The main change happened because bldr had harcoded the path for finding the third party blocks:

     public function getThirdPartyBlocks()
     {
         /** @var Application $application */
         $application = $this->get('application');
-        $blockFile   = $application->getEmbeddedComposer()->getExternalRootDirectory().'/.bldr/blocks.yml';
+        $embeddedComposer = $application->getEmbeddedComposer();
+        $config = $embeddedComposer->getExternalComposerConfig();
+        $loadBlock = $config->has('block-loader') ? $config->get('block-loader') : '.bldr/blocks.yml';
+        $blockFile = $embeddedComposer->getExternalRootDirectory().DIRECTORY_SEPARATOR.$loadBlock;

Instead of hard coding it to /.bldr/blocks.yml now we ask from the embedded composer instance that has already read the bldr.json to give us that information under block-loader key. This is in turn constructed based on the root directory also provided by embedded composer.

Because the ContainerBuilder class in which we were tests for the existence of the blocks.yml file under certain folders we have to come up with a cleaner way to test for file existence without polluting the folders with fixtures.

         "phpunit/phpunit":           "~4.2.0",
+        "mikey179/vfsStream":        "~1.4.0",

Welcome to vfsStream package! This little thing is very good for testing using the stream wrapper from PHP. Let’s see the final test:

<?php
 
/**
 * This file is part of Bldr.io
 *
 * (c) Aaron Scherer <aequasi@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE
 */
 
namespace Bldr\Test\DependencyInjection;
 
use Bldr\DependencyInjection\ContainerBuilder;
use org\bovigo\vfs\vfsStream;
 
/**
 * @author Luis Cordova <cordoval@gmail.com>
 */
class ContainerBuilderTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @var ContainerBuilder
     */
    protected $containerBuilder;
    protected $application;
    protected $input;
    protected $output;
    protected $root;
 
    public function setUp()
    {
        $this->application = $this->getMockBuilder('Bldr\Application') ...
        // ...
 
        $this->root = vfsStream::setup();
    }
 
    public function testGetThirdPartyBlocks()
    {
        $embeddedComposer = $this->getMockBuilder('Dflydev\EmbeddedComposer\Core\EmbeddedComposer')
        // ...
 
        $config = $this->getMockBuilder('Composer\Config')
        // ...
 
        $config->expects($this->once())
            ->method('has')
            ->with('block-loader')
            ->willReturn(true)
        ;
        $config->expects($this->once())
            ->method('get')
            ->with('block-loader')
            ->willReturn('build/blocks.yml')
        ;
        $embeddedComposer
            ->expects($this->once())
            ->method('getExternalComposerConfig')
            ->willReturn($config)
        ;
        $embeddedComposer
            ->expects($this->once())
            ->method('getExternalRootDirectory')
            ->willReturn(vfsStream::url('root'))
        ;
        $this->application
            ->expects($this->once())
            ->method('getEmbeddedComposer')
            ->willReturn($embeddedComposer)
        ;
 
        $bldrFolder = vfsStream::newDirectory('build')->at($this->root);
        vfsStream::newFile('blocks.yml')
            ->withContent('[ \stdClass, \stdClass ]')
            ->at($bldrFolder)
        ;
 
        $this->containerBuilder = new ContainerBuilder(
            $this->application,
            $this->input,
            $this->output
        );
 
        $this->assertCount(2, $this->containerBuilder->getThirdPartyBlocks());
    }
}

I have tried to shorten the mocking part, but you can see clearly that to match a file_exists($file) call from the code we have to create into the vfs system a root project directory and then the folders using the vfsStream::method API. It is pretty simple. One of the things in which I struggle was that for the root project folder you have to provide the reference always, i.e. vfsStream::url(‘root’)).

Now we see there is not a real file created or anything. All is done via the little library vfsStream that plays well with phpunit.

Hope it helps! Please retweet, and after if you want to see the code I have the link to the PR on the top part of this blog post. Thanks for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *