对于像这样的关键内容,使用 PHPUnit 之类的测试框架编写测试非常重要。
像这里描述的那样安装它(你需要梨):
https ://github.com/sebastianbergmann/phpunit/
我还使用虚拟文件系统,因此您的测试文件夹不会变得混乱:https ://github.com/mikey179/vfsStream/wiki/Install
我只是将您的 Route 函数放入一个名为Route.php
. 在同一个目录中,我现在创建了一个test.php
包含以下内容的文件:
<?php
require_once 'Route.php';
class RouteTest extends PHPUnit_Framework_TestCase {
}
要检查它是否一切正常,请打开命令行并执行以下操作:
$ cd path/to/directory
$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 1.50Mb
There was 1 failure:
1) Warning
No tests found in class "RouteTest".
FAILURES!
Tests: 1, Assertions: 0, Failures: 1.
如果出现这种情况,PHPUnit 已正确安装并且您已准备好编写测试。
为了使 Route 功能更好地可测试并且减少与服务器和文件系统的耦合,我对其进行了一些修改:
// new parameter $request instead of relying on server variables
function Route($root, $request_uri, $request_method) {
// vfsStream doesn't support realpath(). This will do.
$root .= '/';
// replaced server variable with $request_uri
$segments = array_filter(explode('/', $request_uri), 'strlen');
if ((count($segments) == 0) || (is_dir($root) === false)) {
return true; // serve index
}
$controller = null;
$all_segments = array_values($segments);
$segments = $all_segments;
while ((is_null($segment = array_shift($segments)) !== true)
&& (is_dir($root . $controller . $segment . '/'))) {
$controller .= $segment . '/';
}
if (is_file($controller = $root . $controller . $segment . '.php')) {
$class = basename($controller . '.php');
// replaced server variable with $request_method
$method = array_shift($segments) ?: $request_method;
require($controller);
if (method_exists($class = new $class(), $method)) {
return call_user_func_array(array($class, $method), $segments);
}
}
// $all_segments variable instead of a call to self::
throw new Exception('/' . implode('/', $all_segments), 404); // serve 404
}
如果请求索引路由,让我们添加一个测试来检查函数是否返回 true:
public function testIndexRoute() {
$this->assertTrue(Route('.', '', 'get'));
$this->assertTrue(Route('.', '/', 'get'));
}
因为您的测试类扩展PHPUnit_Framework_TestCase
了您现在可以使用诸如$this->assertTrue
检查某个语句的计算结果是否为真之类的方法。让我们再次运行它:
$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.
.
Time: 0 seconds, Memory: 1.75Mb
OK (1 test, 2 assertions)
到这个测试通过了!让我们测试是否array_filter
正确删除了空段:
public function testEmptySegments() {
$this->assertTrue(Route('.', '//', 'get'));
$this->assertTrue(Route('.', '//////////', 'get'));
}
$root
如果路由的目录不存在,我们还可以测试是否请求索引路由。
public function testInexistentRoot() {
$this->assertTrue(Route('./inexistent', '/', 'get'));
$this->assertTrue(Route('./does-not-exist', '/some/random/route', 'get'));
}
为了测试比这更多的东西,我们现在需要包含带有方法的类的文件。因此,让我们在运行每个测试之前使用我们的虚拟文件系统来设置包含文件的目录结构。
require_once 'Route.php';
require_once 'vfsStream/vfsStream.php';
class RouteTest extends PHPUnit_Framework_TestCase {
public function setUp() {
// intiialize stuff before each test
}
public function tearDown() {
// clean up ...
}
PHPUnit 对这种事情有一些特殊的方法。该setUp
方法在此测试类中的每个测试方法之前执行。并且tearDown
执行了测试方法之后的方法。
现在我使用 vfsStream 创建一个目录结构。(如果您正在寻找一个教程来做到这一点:https ://github.com/mikey179/vfsStream/wiki是一个很好的资源)
public function setUp() {
$edit_php = <<<EDIT_PHP
<?php
class edit {
public function get() {
return __METHOD__ . "()";
}
public function post() {
return __METHOD__ . "()";
}
}
EDIT_PHP;
$company_php = <<<COMPANY_PHP
<?php
class company {
public function get(\$id = null) {
return __METHOD__ . "(\$id)";
}
}
COMPANY_PHP;
$this->root = vfsStream::setup('controllers', null, Array(
'admin' => Array(
'company' => Array(
'edit.php' => $edit_php
),
'company.php' => $company_php
)
));
}
public function tearDown() {
unset($this->root);
}
vfsStream::setup()
现在使用给定的文件结构和给定的文件内容创建一个虚拟目录。正如你所看到的,我让我的控制器将方法的名称和参数作为字符串返回。
现在我们可以向我们的测试套件添加更多测试:
public function testSimpleDirectMethodAccess() {
$this->assertEquals("edit::get()", Route(vfsStream::url('controllers'), '/controllers/admin/company/edit/get', 'get'));
}
但是这次测试失败了:
$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.
...
Fatal error: Class 'edit.php.php' not found in C:\xampp\htdocs\r\Route.php on line 27
所以$class
变量有问题。如果我们现在使用调试器(或一些)检查 Route 函数中的以下行echo
。
$class = basename($controller . '.php');
我们可以看到$controller
变量包含正确的文件名,但为什么要.php
附加一个?这似乎是一个打字错误。我认为应该是:
$class = basename($controller, '.php');
因为这会删除 .php 扩展名。我们得到了正确的 classname edit
。
现在让我们测试如果我们请求目录结构中不存在的随机路径是否引发异常。
/**
* @expectedException Exception
* @expectedMessage /random-route-to-the/void
*/
public function testForInexistentRoute() {
Route(vfsStream::url('controllers'), '/random-route-to-the/void', 'get');
}
PHPUnit 自动读取此注释并检查Exception
在执行此方法时是否抛出了异常类型,以及异常的消息是否被/random-route-to-the/void
这看起来很有效。让我们检查$request_method
参数是否正常工作。
public function testMethodAccessByHTTPMethod() {
$this->assertEquals("edit::get()", Route(vfsStream::url('controllers'), '/admin/company/edit', 'get'));
$this->assertEquals("edit::post()", Route(vfsStream::url('controllers'), '/admin/company/edit', 'post'));
}
如果我们执行这个测试,我们会遇到另一个问题:
$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.
....
Fatal error: Cannot redeclare class edit in vfs://controllers/admin/company/edit.php on line 2
看起来我们对同一个文件使用include
/require
多次。
require($controller);
让我们将其更改为
require_once($controller);
现在让我们面对你的问题,编写一个测试来检查目录company
和文件company.php
是否相互干扰。
$this->assertEquals("company::get()", Route(vfsStream::url('controllers'), '/admin/company', 'get'));
$this->assertEquals("company::get()", Route(vfsStream::url('controllers'), '/admin/company/get', 'get'));
正如您在问题中所说,在这里我们得到了 404 异常:
$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.
.....E.
Time: 0 seconds, Memory: 2.00Mb
There was 1 error:
1) RouteTest::testControllerWithSubControllers
Exception: /admin/company
C:\xampp\htdocs\r\Route.php:32
C:\xampp\htdocs\r\test.php:69
FAILURES!
Tests: 7, Assertions: 10, Errors: 1.
这里的问题是,我们不知道何时进入子目录以及何时使用 .php 文件中的控制器。所以我们需要明确你想要发生的事情。我假设以下,因为它是有道理的。
- 仅当控制器不包含请求的方法时才输入子目录。
- 如果控制器和子目录都不包含请求的方法,则抛出 404
所以不要像这里搜索目录:
while ((is_null($segment = array_shift($segments)) !== true)
&& (is_dir($root . $controller . $segment . '/'))) {
$controller .= $segment . '/';
}
我们需要搜索文件。如果我们发现一个文件不包含请求的方法,那么我们搜索一个目录。
function Route($root, $request_uri, $request_method) {
$segments = array_filter(explode('/', $request_uri), 'strlen');
if ((count($segments) == 0) || (is_dir($root) === false)) {
return true; // serve index
}
$all_segments = array_values($segments);
$segments = $all_segments;
$directory = $root . '/';
do {
$segment = array_shift($segments);
if(is_file($controller = $directory . $segment . ".php")) {
$class = basename($controller, '.php');
$method = isset($segments[0]) ? $segments[0] : $request_method;
require_once($controller);
if (method_exists($class = new $class(), $method)) {
return call_user_func_array(array($class, $method), array_slice($segments, 1));
}
}
$directory .= $segment . '/';
} while(is_dir($directory));
throw new Exception('/' . implode('/', $all_segments), 404); // serve 404
}
此方法现在按预期工作。
我们现在可以添加更多的测试用例,但我不想更多地扩展它。如您所见,运行一组自动化测试以确保您的函数中的某些内容正常工作非常有用。它对调试也很有帮助,因为您可以知道错误发生的确切位置。我只是想让您开始了解如何进行 TDD 以及如何使用 PHPUnit,以便您可以自己调试代码。
“授人以鱼,养其一日。授人以渔,养其一生。”
当然,您应该在编写代码之前编写测试。
这里还有一些可能很有趣的链接: