このような重要なものについては、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'));
}
テスト クラスが拡張
されるため、特定のステートメントが true と評価されるかどうかを確認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
の s) で調べます。
$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 の使用方法について説明したいと思います。
「人に魚を与えれば、その人を一日養うことができます。人に釣りを教えれば、一生養うことができます。」
もちろん、コードを書く前にテストを書くべきです。
興味深いかもしれないいくつかのリンクを次に示します。