Those of you still reading are hopefully familiar with unit testing. PHPUnit is the de-facto standard for unit testing in PHP projects, and this is what we will be using together with vfsStream in this article.
vfsStream is a custom stream wrapper for PHP that works as a virtual file system that PHP’s built in filesystem functions can use. Some functions that work with vfsStream are:
There are some known limitations when using custom stream wrappers. Some functions that does not work with vfsStream:
vfsStreams’ known limitations are listed on its wiki. Now, on to the code!
The following simplified class is the class we will be testing using PHPUnit and the vfsStream stream wrapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | [code language="php"]class VGF_Storage_Driver_Filesystem { /** * Root directory * * @var string */ public $rootDir = null; /** * Store a file * * @param string $originalPath Path to the original local file * @param string $hash Hash that will be used to identify the new file * @return boolean Returns true on success or false on failure */ public function store($originalPath, $hash) { return copy($originalPath, $this->rootDir . '/' . $hash); } /** * Delete a file * * @param string $hash Hash identifying the file we want to delete * @return boolean Returns true on success or false on failure */ public function delete($hash) { return unlink($this->rootDir . '/' . $hash); } /** * Fetch a file * * @param string $hash Hash identifying the file we want to fetch * @return string Returns the content of the file */ public function get($hash) { return file_get_contents($this->rootDir . '/' . $hash); } }[/code] |
PS! The class is very simplified and does not include error handling at all. It should only be used for instructional purposes.
As you can see the class uses regular file system functions such as copy
, unlink
, and file_get_contents
. To be able to get full code coverage on this class all these functions need to work their magic.
A typical way to solve this when writing tests is to create files/folders in PHPUnit’s setUp()
method and remove them again in the tearDown()
method. This works, but if your tests somehow dies (fatal error for instance) during a test run you are left with files/folders on disk that was never cleaned up. vfsStream does not touch the file system at all, but keeps virtual files and folders in memory, so if your test suite dies there is nothing on disk that was not there before you started the tests. Also, if you manage to sneak in a bug that deletes more files than it should it’s safer to work on a virtual file system than an actual one.
Now, lets take a look at the basics of vfsStream. First you will need to install it. This can be done easily using PEAR:
1 2 | $ pear channel-discover pear.php-tools.net $ pear install pat/vfsStream-beta |
After installation make sure the vfsStream directory is in your include path. If you have a sane PEAR environment you are most likely good to go.
The following snippet will show you the basics of vfsStream:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | [code language="php"]<?php require_once 'vfsStream/vfsStream.php'; vfsStreamWrapper::register(); $root = vfsStream::newDirectory('someDir'); vfsStreamWrapper::setRoot($root); var_dump(is_dir(vfsStream::url('someDir'))); // true var_dump(is_file(vfsStream::url('someDir'))); // false var_dump(is_dir(vfsStream::url('someOtherDir'))); // false $file = vfsStream::newFile('someFile'); $root->addChild($file); var_dump(is_file(vfsStream::url('someDir/someFile'))); // true var_dump(file_exists(vfsStream::url('someDir/someFile'))); // true var_dump(is_file(vfsStream::url('dir/someOtherFile'))); // false unlink(vfsStream::url('someDir/someFile')); var_dump(file_exists(vfsStream::url('someDir/someFile'))); // false[/code] |
First we simply require the main vfsStream
class (that in turn will require other components as well). The next three lines registers the stream wrapper (which defaults to vfs://) and creates the root directory which vfsStream
will use as a container for other virtual files and directories. These three lines can be replaced by:
1 | [code language="php"]$root = vfsStream::setup('someDir');[/code] |
which is a convenience method that will be used in the unit tests later on.
The vfsStream::url
method that is used in the code snippet above creates the path that the file system functions must use. It will simply prepend the schema to the file/directory name.
1 | [code language="php"]var_dump(vfsStream::url('someDir')); // "vfs://someDir"[/code] |
The next few lines in the code snippet just illustrates how the regular file system functions work with vfsStream
.
To add a file we first create a new vfsStreamFile
object by calling vfsStream::newFile()
which simply takes a filename as argument. We attach it to the root directory by using the root object’s addFile
method. After this we do some file system checks for the file, remove it, and check again. As you can see all this is pretty similar to working with regular files.
Now that we have the basic usage of vfsStream out of the way, let’s move on to how to incorporate this into our tests. The below code snippet is the test class without any actual tests as we’ll add them later on.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | [code language="php"]<?php require_once 'vfsStream/vfsStream.php'; class VGF_Storage_Driver_FilesystemTest extends PHPUnit_Framework_TestCase { /** * Driver instance * * @var VGF_Storage_Driver_Filesystem */ protected $driver = null; /** * Setup method * * This method will register the vfsStream stream wrapper and create a new instance of the driver */ public function setUp() { vfsStream::setup('someDir'); $this->driver = new VGF_Storage_Driver_Filesystem(); $this->driver->rootDir = vfsStream::url('someDir'); } /** * Tear down method * * This method will destroy the instance of the driver */ public function tearDown() { $this->driver = null; } }[/code] |
As you can see from the code snippet we use the vfsStream::setup
method to create a root dir, and set that dir as the rootDir
in the driver. No action is needed in the tearDown()
method regarding vfsStream
. The first test we will write shall test the store
method.
1 2 3 4 5 6 7 8 9 10 11 | [code language="php"]/** * Test the store method in the driver */ public function testStore() { $hash = md5(microtime()); $root = vfsStreamWrapper::getRoot(); $this->assertFalse($root->hasChild($hash)); $this->assertTrue($this->driver->store(__FILE__, $hash)); $this->assertTrue($root->hasChild($hash)); }[/code] |
First we create a random hash value that will be used to identify the file internally in our driver. Then we make sure the file does not exist prior to running the store
method. After storing the file we assert that it now exists. The vfsStreamWrapper::getRoot()
method returns the root vfsStreamDirectory
object, on which we call the hasChild()
method to see if it has a child with a given name.
Next up is the delete
method:
1 2 3 4 5 6 7 8 9 10 11 12 | [code language="php"]/** * Test the delete method in the driver */ public function testDelete() { $hash = md5(microtime()); $root = vfsStreamWrapper::getRoot(); $root->addChild(vfsStream::newFile($hash)); $this->assertTrue($root->hasChild($hash)); $this->assertTrue($this->driver->delete($hash)); $this->assertFalse($root->hasChild($hash)); }[/code] |
This is pretty much the same as the previous test method except that we register a virtual file before calling delete()
. After deleting we assert that the file is actually gone. Now for the last test:
1 2 3 4 5 6 7 8 9 10 11 12 | [code language="php"]/** * Test the get method in the driver */ public function testGet() { $hash = md5(microtime()); $content = 'some content'; $file = vfsStream::newFile($hash); $file->setContent($content); vfsStreamWrapper::getRoot()->addChild($file); $this->assertSame($content, $this->driver->get($hash)); }[/code] |
In this last test method we create a virtual file with content by using the setContent()
method on the vfsStreamFile
object. After adding the virtual file we assert that we get the same content when fetching the file identified by $hash
.
And that’s that! If anyone has used vfsStream differently or has related questions, don’t hesitate to leave a comment. Happy testing!
Related links: