其實我不會寫測試。
其實我不會寫測試。
其實我不會寫測試。
官方的測試工具
其實 PhalconPHP 有提供 Unit testing 測試的小小範例,但是他就真的跟飯粒一樣小,以致於我完全不知到該怎麼辦才好。加上 PHPUnit 我不熟(前情提要:我不會寫測試。所以基本上該踩的雷還是要踩一踩才知道該怎麼辦。
官方提供的開發工具庫 phalcon/incubator
裡面,確實有提供 Tests 的範例給你用,但是,他的 UnitTestCase.php
怪怪的,
protected function setUp()
{
$this->checkExtension('phalcon');
// Reset the DI container
Di::reset();
// ... 以下省略
當你在 phpunit.xml
當中設定好 bootstrap="./bootstrap.php"
,然後你在 bootstrap.php
也寫好相關的初始化設定,然後你開始寫測試,然後你的 DI 就被他給 Di::reset()
掉了(......
另外,由於 PhalconPHP 在 processIsolation
要設定為 false
,然後接著衍生另外一個問題,如果跑兩個 TestCase 的時候,你在 bootstrap
做的初始化的 DI 會被清掉(疑!?為什麼?
所以基本上官方的工具不是不能用,而是要改了才能用(但是這個開發工具庫好像很久沒更新了?
不然就自己來
當然你可以直接 extends \PHPUnit_Framework_TestCase
然後都自己做,這樣是比較保險的一件事情。但是,你也不可能每一個 TestCase 都把初始化的事情都做一遍,這樣也太麻煩了點。
所以基本上我遇到了幾個問題,
- 預設的 DI 會被清除
- bootstrap 的初始化 DI 會消失
- Functional Test 的
handle
有問題 - Model Test 是不是真的要連 DB
DI 消失的問題
更新:感恩 Steve Lo,讚嘆 Steve Lo
因為 tearDown()
強迫做了 $di::reset()
所以每次的 TestCase 都會被重新設定,這也是為何 bootstrap 裡面的 DI 會消失的主因。
但是,為何 reset()
之後會變成 NULL
則不得而知(因為就算是 reset()
了,基本上他應該還是要保有 \Phalcon\DiInterface
的特性才對,而不應該是 NULL
。
所以就這樣做就好,
protected function tearDown()
{
// 不要幫我 reset() 我還有很多 Test Case 要做
// $di = $this->getDI();
// $di::reset();
parent::tearDown();
}
protected function setUp()
{
$this->checkExtension('phalcon');
// 這裡就是先拿拿看 DI 看是不是有東西
$this->di = \Phalcon\Di::getDefault();
// DI 沒東西就重新設定一組
if (false === ($this->di instanceof \Phalcon\DiInterface)) {
\Phalcon\Di::reset();
// ... 把 bootstrap 所有 DI 的設定拿來這邊做
}
// ... 中間省略
// ... 基本上也可以做一些 DI 設定
$this->_loaded = true;
}
這樣基本上 DI 就不會消失,然後你跑多組 TestCase 的時候,DI 也不會莫名其妙的變成 null
,我覺得應該是我誤會了 bootstrap
的用途(不然就是官方 TestHelper.php
寫完之後,根本沒發現 DI 被重設了 Orz...
Functional Test
基本上這樣測試其實有點詭譎啦,因為你叫起一個 $application
然後丟 URI 給他,感覺很像是在做 Router Testing,然後其實你是要測試你的 Controllers 的功能(所以我才說這樣看起來有點怪。
如果你只是要做 Router Testing,就跟這個無關。
所以這個 Functional Test 其實測試了兩件事情,第一是 Router,第二是 Controller。
那麼,$application->handle
發生什麼問題?
- 因為是 CLI 所以無法傳遞 HTTP Method,這在 Router 層就會被檔掉
- 傳回值很有可能直接輸出(類似 stdout,然後你抓不到
首先先解決簡單的,$application
的傳回值,直接用 ob_start()
包起來,然後用 ob_get_contents()
去拿就好了(懂?
ob_start();
$this->application->handle($url);
$response = ob_get_contents();
ob_end_clean();
另外一個比較麻煩,根據耙梳 Router 與 Route 跟 Application 關於 handle
的原始碼之後,基本上只要一行就解決(疑?
$_SERVER['REQUEST_METHOD'] = 'GET';
當然,你不可能每次寫 TestCase 每次都去看你的 Router 到底是哪一種方法,所以最快的方式還是把 Routes 抓出來比(鞭)對(屍)這樣比較方便,
$routes = $this->application->router->getRoutes();
$methods = NULL;
foreach($routes as $route) {
$pattern = $route->getCompiledPattern();
if (substr($pattern, 0, 1) !== '#') {
$pattern = '#^'.$pattern.'$#';
}
if (preg_match($pattern, $url)) {
$methods = $route->getHttpMethods();
break;
}
}
if (false === is_null($methods) && is_string($methods)) {
$_SERVER['REQUEST_METHOD'] = $methods;
}
另外一個問題,如果我的 Route 是 via('GET', 'POST')
怎麼辦?好問題,這樣當然就只能把 $_SERVER['REQUEST_METHOD']
搬到你的 TestCase 裡面去做啦,不然,誰會知道你想要測試的方法是 GET
還是 POST
?
Model Test
預設還是會連到資料庫去(反正是測試機?有差嗎?倒是,官方提供的 ModelTestCase.php
有一個怪怪的地方,
/**
* Empties a table in the database.
*
* @param string $table
* @return boolean
*/
public function emptyTable($table)
{
$connection = $this->di->get('db');
$success = $connection->delete($table);
return $success;
}
是不是我對 Empty 這個字有誤會?
他用 delete 耶...
誰測試用這種 Method 我替你阿彌陀佛...
所以我自己改寫的時候換成 return true
(欸,我當然不希望測試的時候真的把資料庫清掉,畢竟資料還是有用的嘛(而且資料表不能清空或是不能刪除,絕對不會是你的 Model 寫錯(懂?
補一下 Router 測試
跟 Functional Test 很類似,但是不交給 $application
去做了,就從 $application
提出 router
來測試,或是,如果你的 Router 是獨立檔案,那就丟給 DI 就可以了。
$this->di->set(
'router',
function() use ($application) {
// 如果你的 Router 是獨立檔案。
require __DIR__.'/../../app/router.php';
// 如果不是
$router = $application->router;
return $router;
}
);
然後你可以做一個小小的方法給你的 TestCase 共用,
public function testRouter($method = null, $uri, $module, $controller, $action, $params)
{
if (!is_null($method) && is_string($method)) {
$_SERVER['REQUEST_METHOD'] = $method;
}
$router = $this->di->get('router');
$router->handle($uri);
$this->assertEquals($router->getModuleName(), $module);
$this->assertEquals($router->getControllerName(), $controller);
$this->assertEquals($router->getActionName(), $action);
$this->assertEquals($router->getparams(), $params);
}
大概就這樣,Router 這邊其實還比較好處理,畢竟他沒有輸出的問題。
小結
我還是不會寫測試。