[PhalconPHP] 測試 Route 與 Controller 小技巧

其實我不會寫測試。

其實我不會寫測試。

其實我不會寫測試。


官方的測試工具

其實 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 都把初始化的事情都做一遍,這樣也太麻煩了點。

所以基本上我遇到了幾個問題,

  1. 預設的 DI 會被清除
  2. bootstrap 的初始化 DI 會消失
  3. Functional Test 的 handle 有問題
  4. 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();
    }
首先是 DI,只能自己寫一個 `UnitTestCase.php` 然後把 `setUp()` 改一下,

    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 這邊其實還比較好處理,畢竟他沒有輸出的問題。

小結

我還是不會寫測試。

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei