vfsStream简介
- 什么是vfsStream
vfsStream是基于PHP流包装器(stream warpper)实现的虚拟文件系统,可在单元测试中模拟真实文件系统。
PHP中的stream wrapper允许用户实现自定义的协议、协议处理器和流。一旦自定义协议被创建,便可以使用mkdir(),fopen(),fread()等函数来处理基于此协议的流。如果想要创建自定义协议需要满足一些条件,更多的信息可以查看PHP手册。
- 为什么要使用vfsStream
vfsStream可在单元测试中用来模拟文件系统,以达到测试代码与真实文件系统解耦的目的。在vfsStream之上使用mkdir()等方法不会在磁盘上产生任何垃圾文件,一切操作都在内存中进行。一个较常见的例子是:
待测试的类——检测磁盘上是否存在某一文件夹,若不存在则创建。
<?php
class Example {
public function __construct($id) {
$this->id = $id;
}
public function setDirectory($dir) {
$this->dir = $dir . '/' . $this->id;
if (file_exists($this->dir) === false) {
mkdir($this->dir, 0700, true);
}
}
}
传统的测试用例如下所示:
<?php
class ExampleTest extends PHPUnit_Framework_TestCase {
$DIR = dirname(__FILE__);
public function setUp() {
if (file_exists($DIR . '/id')) {
rmdir($DIR . '/id');
}
}
public function tearDown() {
if (file_exists($DIR . '/id')) {
rmdir($DIR . '/id');
}
}
public function testDirectoryIsCreated() {
$example = new Example('id');
$this->assertFalse(file_exists($DIR . '/id'));
$example->setDirectory($DIR);
$this->assertTrue(file_exists($DIR . '/id'));
}
}
传统测试用例的一大问题就是在测试用例运行时会在文件系统上创建“垃圾文件”,所以需要在setUp()和tearDown()方法中做清理工作。
使用vfsStream的测试用例:
<?php
class ExampleTest extends PHPUnit_Framework_TestCase {
public function setUp() {
vfsStreamWrapper::register();
$root = new vfsStreamDirectory('aDir');
vfsStreamWrapper::setRoot($root);
}
public function testDirectoryIsCreated() {
$url = vfsStream::url('aDir/id');
$example = new Example('id');
$this->assertFalse(file_exists($url));
$example->setDirectory(vfsStream::url('aDir'));
$this->assertTrue(file_exists($url));
}
}
可以发现,使用vfsStream以后,不需要在setUp()或tearDown()方法里做任何清理工作。因为vfsStream的一次操作都在内存中进行。
- 我如何使用vfsStream
我是在阅读PHPUnit手册时发现这个有趣,好用的东东。在Restiny的测试用例中,vfsStream被用于创建文件系统中的配置文件,以便ConfigLoader读取。代码如下:
ConfigLoader Class:
<?php
/**
* 配置载入器
*
*
*/
class ConfigLoader {
public function loadConfig($configFileName, $configDirectoryPath = '') {
if (empty($configDirectoryPath)) {
$configDirectoryPath = APP_PATH . DIRECTORY_SEPARATOR . 'config';
}
$configFilePath = $configDirectoryPath . DIRECTORY_SEPARATOR . $configFileName . '.php';
if (!file_exists($configFilePath)) {
throw new RestinyFileNotFoundException($configFileName . ' config file not found', 0);
}
return require_once $configFilePath;
}
}
ConfigLoader所做的工作便是在$configDirectoryPath执行的路径下寻找文件名为$configFileName的配置文件。如果未找到,则抛出异常。
ConfigLoader的测试用例如下:
<?php
/**
* 测试用例-配置载入器
*
*/
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'testBootstrap.php';
require_once dirname(__FILE__) . '/http://www.cnblogs.com/core/ConfigLoader.php';
require_once 'vfsStream/vfsStreamDirectory.php';
require_once 'vfsStream/vfsStreamWrapper.php';
require_once 'vfsStream/vfsStream.php';
/**
* Test class for Config.
* Generated by PHPUnit on 2011-05-04 at 04:05:20.
*/
class ConfigLoaderTest extends PHPUnit_Framework_TestCase {
private $_rootConfigDir = 'configDir';
private $_configDir = 'resources';
private $_configFileName = 'resources';
private $_configFile = 'resources.php';
protected function setUp() {
vfsStream::setup($this->_rootConfigDir);
}
protected function tearDown() {
}
public function testFailedLoadConfig() {
$this->setExpectedException('RestinyFileNotFoundException', 'resources1 config file not found');
ConfigLoader::loadConfig('resources1');
}
/**
*
*/
public function testLoadConfig() {
$dir = vfsStream::newDirectory($this->_configDir)->at(vfsStreamWrapper::getRoot());
$file = vfsStream::newFile($this->_configFile)->at($dir);
$data= array(
'/feed/(?<name>[a-zA-Z_0-9]+)' => '/feed',
'a' => '2',
'b' => 3
);
$fileContent = '<?php' . PHP_EOL;
$fileContent .= 'return ';
$fileContent .= var_export($data, true);
$fileContent .= ';';
$file->setContent($fileContent);
$config = ConfigLoader::loadConfig($this->_configFileName, vfsStream::url($this->_configDir));
$this->assertEquals($data, $config);
$this->assertEquals('2', $config['a']);
$this->assertEquals(2, $config['a']);
$this->assertNull($config['c']);
}
}
?>
下面分开讲解一下这个测试用例。
测试用例ConfigLoaderTest在setUp()方法中调用了vfsStream类的静态方法setUp()。代码如下:
protected function setUp() {
vfsStream::setup($this->_rootConfigDir);
}
vfsStream::setup是对vfsStream启动流程的一个封装,函数原型如下:
public static function setup($rootDirName = 'root', $permissions = null)
{
vfsStreamWrapper::register();
$root = self::newDirectory($rootDirName, $permissions);
vfsStreamWrapper::setRoot($root);
return $root;
}
之后在testLoadConfig中,有如下代码:
$dir = vfsStream::newDirectory($this->_configDir)->at(vfsStreamWrapper::getRoot());
这行代码的作用是创建一个文件夹并将它添加到跟节点上。此时,该文件夹的url为 vfs://root/dirName
接着代码将执行:
$file = vfsStream::newFile($this->_configFile)->at($dir);
这行代码的作用是创建一个文件并将它添加到之前创建的文件夹下。此时,该文件的url为vfs://root/dirName/fileName
接下来:
$data= array( '/feed/(?<name>[a-zA-Z_0-9]+)' => '/feed', 'a' => '2', 'b' => 3 ); $fileContent = '<?php' . PHP_EOL; $fileContent .= 'return '; $fileContent .= var_export($data, true); $fileContent .= ';'; $file->setContent($fileContent);
这几行代码的作用是构造一个字符串作为新文件的内容,并使用setContent()方法将内容写入文件。
$config = ConfigLoader::loadConfig($this->_configFileName, vfsStream::url($this->_configDir));
$this->assertEquals($data, $config);
$this->assertEquals('2', $config['a']);
$this->assertEquals(2, $config['a']);
$this->assertNull($config['c']);
接下来将欲载入的配置文件名和配置文件所在文件夹路径传入ConfigLoader::loadConfig方法,并断言。
以上代码展示了vfsStream一些最基本的功能。vfsStream还可以对文件或文件夹权限、属组进行模拟,功能十分强大。
下图是我绘制的vfsStream的类图,可让大家对vfsStream的结构有一个简单的认识。

vfsStream资源:
项目主页:http://code.google.com/p/bovigo/wiki/vfsStream
<vfsStream - effective filesystem mocking>:http://www.slideshare.net/proofek/vfsstream-effective-filesystem-mocking
<vfsStream - A better approach for file system dependent tests>: http://talks.frankkleine.de/ipc_vfsStream.pdf