对于像这样的关键内容,使用 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 变量有问题。如果我们现在使用调试器(或一些 echos)检查 Route 函数中的以下行。
$class = basename($controller . '.php');
我们可以看到$controller 变量保存了正确的文件名,但是为什么附加了.php?
这似乎是一个打字错误。我认为应该是:
$class = basename($controller, '.php');
因为这会删除 .php 扩展名。我们得到了正确的类名edit。
现在让我们测试一下,如果我们请求目录结构中不存在的随机路径,是否会引发异常。
/**
* @expectedException Exception
* @expectedMessage /random-route-to-the/void
*/
public function testForInexistentRoute() {
Route(vfsStream::url('controllers'), '/random-route-to-the/void', 'get');
}
PHPUnit 会自动读取该 cmets 并检查在执行此方法时是否抛出 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,所以你可以自己调试你的代码。
“授人以鱼,养其一日。授人以渔,养其一生。”
当然,您应该在编写代码之前编写测试。
这里还有一些可能很有趣的链接: