Richard's Blog

Laravel Storage and Parallel Testing

November 19, 20182 min read • Last updated May 31, 2020

Laravel provides neat testing helpers for working with the filesystem — however if you run your tests in parallel, issues are inevitable.

File Storage in real life (by Samuel Zeller)

File Storage in real life (by Samuel Zeller)

Problem

At Exposify we use the Storage Facade of Laravel to fluently access files and store data. It also makes writing feature tests much easier because it automatically swaps directories and cleans them up before running other tests with Storage::fake('my-disk').

We also use Paratest to let our PhpUnit Tests run much quicker by using multiple processes.

The thing is: As soon as you have multiple processes running, some tests will be executed before others are finished. This means that Laravel will clean up old directories for the upcoming test even when the other test still needs the files in those directories.

This results in all sorts of exceptions related to the file system. Not cool.

Solution

The default implementation of Storage::fake() as of 7.0 looks like this:

<?php
// ...
public static function fake($disk = null, array $config = []) {
$disk = $disk ?: static::$app['config']->get('filesystems.default');
(new Filesystem)->cleanDirectory(
$root = storage_path('framework/testing/disks/'.$disk)
);
static::set($disk, $fake = static::createLocalDriver(array_merge($config, [
'root' => $root,
])));
return $fake;
}
// ...

As you can see every time we call Storage::fake('foo') Laravel will empty the directory for the disk foo in the place where the temporary filesystem lives. Then it’s gonna initiate the system with the new path for the disk.

What we want however is a truly unique storage location every time the storage is faked — so our tests cannot interfere with each other.

We can achieve this by initiating the system on our own with a unique root path. After the test is done, we’ll remove this unique directory to not clutter our environment.

In this case I created a trait CreatesFakeStorage:

<?php
namespace Tests;
use Storage;
use Illuminate\Filesystem\Filesystem;
trait CreatesFakeStorage
{
/**
* Replace the given disk with a local testing disk. We use a unique
* base path for every test to prevent issues with the filesystem
* when running tests in a multi process environment.
*
* @param string|null $disk
* @return void
*/
protected function fakeStorage($disk = null)
{
$disk = $disk ?: $this->$app['config']->get('filesystems.default');
$time = (int) (microtime(true) * 1000);
$base = storage_path('framework/testing/' . $time);
$root = $base . '/disks/' . $disk;
$this->beforeApplicationDestroyed(function() use ($base) {
(new Filesystem)->deleteDirectory($base);
});
Storage::set($disk, Storage::createLocalDriver(['root' => $root]));
}
}

This trait can now be used in the base Tests\TestCase.php or in special Test Case classes. One just needs to replace the old Storage::fake('foo') with $this->fakeStorage('foo').

The method performs similar actions as the original one, except that it assigns a unique time stamped path to each directory and then removes this folder and its content when the test is about to be torn down. Easy as that.

I admit the discussed scenario is a special case that not many will encounter. However if you found this article helpful, let me know! If not, keep it to you. Or tell me so I can work on it.


Got thoughts on this? Write me a response!


I write articles to get a better understanding of software and communication topics.

Get notified about new posts

I'll send you a notification when there's new content. (Privacy)

Previous Post:

Next Post:

About the basics of deep neural networks

April 02, 20205 min read

As early as in the 1940s people started to think about how to mimic the human brain. But only since about 2006 has the right hardware and large amount of collected data enabled us to effectively pursue and research this option.