Test Laravel filesystem storage with Virtual File System

Reading time ~5 minutes

Introduction

Since Laravel 5, the filesystem operations are abstracted thanks to the Flysystem PHP package by Frank de Jonge. This integration provides simple drivers for working with local filesystems, Amazon S3, and other Cloud Storage. The API remains the same for each system therefore is really simple to swap between each storage option.

Testing the filesystem is not an easy task. I covered some of the challenges in a previous blog post, but luckily we have a couple of tools at our disposal that can simplify considerably our work.

Flysystem Adapters

As I said, Laravel uses the Flysystem library to abstract the filesystem. This library makes use of the adapter pattern:

“the inconsistencies of the different file systems are leveled out by adapters, which translate requests into calls the file system understands and re-format responses to comply with the interface of the generic file system.”

Flysystem provides all different kinds of adapters, that basically cover everything you need: local filesystem, AWS, Dropbox, Rackspace, FTP etc.

For testing purposes Flysystem provides three Adapters:

  • Memory: the filesystem is keeped completely in memory
  • Null/Test: acts like /dev/null
  • VFS: allows to mount a virtual filesystem

For this post we will examine the last one. The idea is to swap the local adapter with the VFS when the application is being tested. We will use PHPUnit as our main testing tool.

You can install the adapter through composer:

composer require league/flysystem-vfs

The code to test

Now let’s write a simple class that is responsible for writing invoices on disk, after a generic order has been performed on the site. We will keep things as simple as possible.

<?php

class InvoiceService
{
    protected $storage;

    public function __construct(\Illuminate\Contracts\Filesystem\Factory $storage)
    {
        $this->storage = $storage;
    }

    public function generateInvoiceFromOrder($order)
    {
        if(! $this->storage->has('/invoices') {
            $this->storage->makeDirectory('/invoices');
        }

        $invoice_path = "/invoices/{$order->id}.txt";

        $this->storage->put($invoice_path, "This is the invoice for the order {$order->id}");

        return $invoice_path;
    }
}

We are passing the filesystem Factory contract in our constructor, which is later resolved by Laravel using Dipendency Injection into the FilesystemManager implementation. In this way, later in the test, we can inject our version of the Filesystem class, properly extended with VFS support.

The public method generateInvoiceFromOrder checks that the /invoice directory exists, otherwise it creates it, and then it saves a new file named after the order id before returning its path.

It’s important to note that each call to $this->storage uses the default filesystem disk (usually the local one) defined in the filesystems.php configuration file. We are going to use this behaviour at our advantage by setting VFS as default disk during our tests.

Notice: with the local filesystem all file operations are relative to the root directory defined in your configuration file. By default, this value is set to the storage/app directory.

Set up the test environment

First add a new variable to your environment .env file to specify the default filesystem used by the application:

APP_FILESYSTEM=local

now we can reference this variable in the filesystems.php configuration file

<?php

'default' => env('APP_FILESYSTEM', 'local')

In the same file add the configuration for the new VFS disk adapter

<?php

'disks' => [
    'local' => [
        'driver' => 'local
        'root'   => storage_path().'/app'
    ],
    'virtual' => [
	   'driver' => 'vfs'
    ]

Finally, before writing our tests, we have to tell PHPUnit that Laravel should use the virtual disk during testing. Therefore we have to override the APP_FILESYSTEM variable the phpunit.xml file

<php>
   <env name="APP_ENV" value="testing"/>
   <env name="APP_FILESYSTEM" value="virtual"/>

Test it!

Now we can start writing our test case. Here is the code:

<?php

use League\Flysystem\Vfs\VfsAdapter;
use League\Flysystem\Filesystem;
use VirtualFileSystem\FileSystem as Vfs;

class InvoiceServiceTest extends TestCase
{
    /** @test */
    public function it_should_generate_an_invoice_file_from_an_order()
    {
        $storage = new Illuminate\Filesystem\FilesystemManager($this->app);

        $vfs = new Vfs();
        $vfs->createStructure([
            'app' => [
                'invoices' => []
            ]
        ]);

        $adapter = new VfsAdapter($vfs);
        $filesystem = new Filesystem($adapter);

        $storage->extend('vfs', function() use ($filesystem) {
            return $filesystem;
        });

        $order = new Order();
        $order->id = 1;

        $invoiceService = new \App\Services\InvoiceService($storage);


        $destination_path = $invoiceService->generateInvoiceFromOrder($order);


        $this->assertEquals('/app/invoices/1.txt', $destination_path);
        $this->assertTrue($filesystem->has('/app/invoices/1.txt'));
    }
}

Inside our test we first create a new instance of the FilesystemManager class to pass to our InvoiceService. Then we conveniently create our VFS directory structure using an array, to match the local disk, and finally we build the VfsAdapter and the Filesystem class that will be used by Laravel.

The most important step here is to tell the FilesystemManager to use this adapter instance, otherwise it couldn’t be able to associate the ‘vfs’ driver defined in the filesystems.php config with the actual implementation. Luckily for us, Laravel provides the extend method which uses a closure to register a custom driver.

<?php

$storage->extend('vfs', function() use ($filesystem) {
    return $filesystem;
});

The rest of the code is straightforward. We assert that the destination path matches our expectation and finally we check that the file has been actually persisted on the file system using the Flysystem API.

Trick: you can check that the function actually uses the VFS implementation by dumping the value of the path prefix using this instruction dd($this->storage->getDriver()->getAdapter()->getPathPrefix()); In case of a VFS implementation you should get something like phpvfs:56c86e3136cf9://.

Processwire Basic Website Workflow Part 2

—#layout: post#title: Basic ProcessWire website workflow - Part Two#description: “Part two of a basic workflow/tutorial for building simp...… Continue reading