我不再写博客了。我所做的大部分工作都是为了我的雇主Zend,我觉得不能随意谈论它(其中一些确实是机密的)。不过,我可以说过去几个月我一直在大量使用PHP5进行编程,并且有机会做一些非常有趣的事情。在我能够使用的新事物中,有SPL和PHPUnit,最近还一起使用。
我之前写过关于单元测试的文章,以及我对PEAR中使用的phpt式测试的偏好。但是,由于ZendFramework使用PHPUnit2,而我在Zend工作……我必须入乡随俗。
我实际上已经开始喜欢PHPUnit2风格的测试了。最后,我发现我的测试比我用phpt执行它们的方式要简洁得多,而且我倾向于测试失败而不是成功;失败应该是规则的例外。无数的“断言”方法使这变得相对容易(尽管有些方法以意想不到的方式运行——例如,尝试在包含PDO句柄的两个对象上测试assertSame())。
我缺少的一件事是在一个目录中运行所有测试的简单方法,alapearrun-tests。我阅读了PocketGuide,并注意到创建测试套件以自动运行测试的可能性。(实际上,新版本的PEAR现在支持通过pearrun-tests运行PHPUnit测试,只要有一个名为AllTests.php的文件包含测试中的测试套件目录。)
然而,一开始我很失望。演示的方法是手动要求每个测试文件并将其中包含的类添加到测试套件中。基本上,每次我向套件添加测试类时,我都需要修改该文件。呸!
所以,我开始考虑它,并意识到我可以遍历目录树,抓取与模式/(.*?Test)\.php\$/匹配的文件,加载它们向上,并将它们各自的类添加到套房。
最初,我打算结合使用opendir()、readdir()和closedir(),以及然后想,“我正在用PHPUnit做一些新的事情,为什么不继续学习并用SPL来做呢?”
SPL的问题在于没有很好地记录它。它有大量的API文档,但主要是这样的,“存在某某类,具有某某属性和方法”。如果存在任何用例,它们通常在用户提供的评论中。我知道,如果这是一个问题,放下我的蠢事并解决它——也许我会,当我有一周左右的空闲时间时。
幸运的是,在DirectoryIterator::construct()条目的注释中有一个很好的RecursiveDirectoryIterator用例。需要注意的一件事:您不能将foreach()与RecursiveDirectoryIterator一起使用,因为您不仅需要访问array元素,而且迭代器本身;因此需要一个for()循环。
有了RecursiveDirectoryIterator之后,我就能够快速创建一个测试套件:
<?php
if (!defined('PHPUnit2_MAIN_METHOD')) {
define('PHPUnit2_MAIN_METHOD', 'AllTests::main');
}
require_once 'PHPUnit2/Framework/TestSuite.php';
require_once 'PHPUnit2/TextUI/TestRunner.php';
class AllTests
{
/**
* Root directory of tests
*/
public static $root;
/**
* Pattern against which to test files to see if they contain tests
*/
public static $filePattern;
/**
* Pattern against which to test directories to see if they are for source
* code control metadata
*/
public static $sscsPattern = '/(CVS|\.svn)$/';
/**
* Associative array of test class => file
*/
public static $list = array();
/**
* Main method
*
* @static
* @access public
* @return void
*/
public static function main()
{
PHPUnit2_TextUI_TestRunner::run(self::suite());
}
/**
* Create test suite by recursively iterating through tests directory
*
* @static
* @access public
* @return PHPUnit2_Framework_TestSuite
*/
public static function suite()
{
$suite = new PHPUnit2_Framework_TestSuite('MyTestSuite');
self::$root = realpath(dirname(__FILE__));
self::$filePattern = '|^' . self::$root . '/(.*?Test)\.php$|';
self::createTestList(new RecursiveDirectoryIterator(self::$root));
foreach (self::$list as $class => $file) {
require_once $file;
$suite->addTestSuite($class);
}
return $suite;
}
/**
* Recursively iterate through a directory looking for test classes
*
* @static
* @access public
* @param RecursiveDirectoryIterator $dir
* @return void
*/
public static function createTestList(RecursiveDirectoryIterator $dir)
{
for ($dir->rewind(); $dir->valid(); $dir->next()) {
if ($dir->isDot()) {
continue;
}
$file = $dir->current()->getPathname();
if ($dir->isDir()) {
if (!preg_match(self::$sscsPattern, $file)
&& $dir->hasChildren())
{
self::createTestList($dir->getChildren());
}
} elseif ($dir->isFile()) {
if (preg_match(self::$filePattern, $file, $matches)) {
self::$list[str_replace('/', '_', $matches[1])] = $file;
}
}
}
}
}
/**
* Run tests
*/
if (PHPUnit2_MAIN_METHOD == 'AllTests::main') {
AllTests::main();
}
该类的关键是createTestList()方法:
public static function createTestList(RecursiveDirectoryIterator $dir)
{
for ($dir->rewind(); $dir->valid(); $dir->next()) {
if ($dir->isDot()) {
continue;
}
$file = $dir->current()->getPathname();
if ($dir->isDir()) {
if (!preg_match(self::$sscsPattern, $file)
&& $dir->hasChildren())
{
self::createTestList($dir->getChildren());
}
} elseif ($dir->isFile()) {
if (preg_match(self::$filePattern, $file, $matches)) {
self::$list[str_replace('/', '_', $matches[1])] = $file->__toString();
}
}
}
}
基本上,您将遍历目录的每个元素。RDI的isDot()方法允许您快速识别.和..条目并跳过它们。isDir()和isFile()让您可以使用漂亮的OOP语法快速识别目录和文件。hasChildren()让您决定是否需要进入目录;getChildren()为子目录返回一个新的RDI对象。
~~更有趣的是对象作为字符串的用法。$dir->current()实际上返回一个SplFileObject。但是,因为它有一个已定义的__toString()方法,您可以在需要字符串的情况下使用它——例如我在这里执行的preg_match()。对于SplFileObject,__toString()方法返回文件的完整路径——这比使用时要方便得多readdir(),它只提供基本名称,因为您可以更方便、更轻松地对提供的文件执行操作(例如require、file_get_contents()等)~~更新:事实证明,DirectoryIterator在PHP5.0.x和5.1.x中的实现方式存在一些差异。因此,我修改了它,改为使用敏捷接口拉取pathName()。
使用RDI的工作实际上大致等同于使用readdir(),除了我不必跟踪文件的路径—这实际上是一个相当大的益处。当RegexFindFile将其纳入核心版本时会更容易—这将允许您执行类似以下操作:
$files = new RegexFindFile(realpath(dirname(__FILE__)), '/Test\.php$/');
$files = iterator_to_array($files);
foreach ($files as $file) {
// We're just working on filenames now... and we have the full list!
//...
}
所以,最后,您会得到一个AllTests.php文件,您可以编写一次,而无需再次触及,假设您为测试命名一致。
