PHPUnit — Mocking the File System using vfsStream

Recently I found myself needing to write tests for a small class that read from a json file. The class needed to read a json file, validate its existence and content, provide a method to inform the user if a certain key exists, and provide a method to retrieve a value for a given key. The class looked something like:

<?php
use FileNotFoundException;
use InvalidJSONException;
class JSONReader
{
  private $file_path;
private $file_contents;
  public function __construct($file_path)
{
$this->file_path = $file_path;
$this->validateFileExists();
$this->validateJSON();
}
  public function hasKey($key)
{
return (array_key_exists($key, $this->file_contents) === true);
}
  public function getValue($key)
{
return $this->hasKey($key) ?
$this->file_contents[$key] :
null;
}
  private function validateFileExists()
{
if (file_exists($this->file_path) === false) {
throw new FileNotFoundException('File not found');
}
}
  private function validateJSON()
{
if ($this->getJSONContents() === null) {
throw new InvalidJSONException('Missing or invalid json');
}
}

private function getJSONContents()
{
if (isset($this->file_contents) === false) {
$this->file_contents = json_decode(
file_get_contents($this->file_path),
true
);
}
     return $this->file_contents;
}
}

I’d like to write tests to validate that:

  • Exceptions are thrown when missing file or invalid json
  • True and false are returned when expected in hasKey()
  • getValue() returns a value for an existent key

Testing this class in isolation can be tricky because it currently has a dependency on the file system. Storing test json files to test this class would work, but is not ideal because it leaves a dependency on the file system in your tests. As with any external resource, there might be intermittent problems with the file system and could result in some flaky tests.

This is where vfsStream shines. vfsStream is a stream wrapper for a virtual file system. Essentially it allows you to define a file system structure, and provides you a path that your tests will be able to read/write/manipulate files from. This makes testing code that uses the php native functions for file manipulation super easy! Here’s what my tests end up looking like.

<?php
use org\bovigo\vfs\vfsStream;
class JSONFileSecretProviderTest extends TestCase
{

public function setUp()
{
// define my virtual file system
$directory = [
'json' => [
'valid.json' => '{"VALID_KEY":123}',
'invalid.json' => '{"test":123'
]
];
// setup and cache the virtual file system
$this->file_system = vfsStream::setup('root', 444, $directory);
}
  /**
* @expectedException FileNotFoundException
*/
public function testFileNotFoundExceptionIsThrownWhenNoFile()
{
$json_reader = new JSONReader(
$this->file_system->url() . 'no-file.json'
);
}
  /**
* @expectedException InvalidJSONException
*/
public function testInvalidJSONExceptionIsThrownWhenNoFile()
{
$json_reader = new JSONReader(
$this->file_system->url() . '/json/invalid.json';
);
}
   public function testHasKeyReturnsTrueWhenKeyFoundInFile()
{
$json_reader = new JSONReader(
$this->file_system->url() . '/json/valid.json'
);
      $this->assertTrue($json_reader->hasKey('VALID_KEY'));
}
   public function testHasKeyReturnsFalseWhenKeyNotFoundInFile()
{
$json_reader = new JSONReader(
$this->file_system->url() . '/json/valid.json'
);
      $this->assertFalse($json_reader->hasKey('NON_EXISTENT'));
}
   public function testGetKeyReturnsValueInFile()
{
$json_reader = new JSONReader(
$this->file_system->url() . '/json/valid.json'
);
      $this->assertEquals(
$json_reader->getSecret('VALID_KEY'),
123
);
}
}

In the above example, the setUp() method defines a directory structure to have a directory json with two files named valid.json and invalid.json with their content being provided. It then calls vfsStream::setup() which takes in a root directory name, permissions for the directory, and the defined directory array. The virtual file system is stored for reference within the tests. At any point in the tests you can call $file_system->url() which will return a full file path to the root directory in your file system, it’s output will look like vfs://root/. After all setup is done, tests can pass in their virtual file paths and native functions like file_get_contents() will read from your virtual file system.

I found this approach to be much cleaner as it removes the file system dependency, gives the developer full control on what their testing file system looks like, and does not require storing any test files your tests might need. I think it becomes even more valuable when your testing code that creates files or directories because there is no clean up required afterwards!

Originally published on https://engineering.weebly.com/