[PhalconPHP] Functional Test 與 PHPUnit mock 愛恨情愁

我不會寫測試第二蛋之我不會寫測試。俗話說 代致不是憨人想的那麼簡單。


PHPUnit Mock

在 PHPUnit 當中,有一個叫做 Mock 的東西,翻譯成中文大概可以叫做模仿。簡單來說,就是可以幫你的類產生出一個 替身,當然不是這種 替身

Mock

好處是,你可以使用這個替身來做一些測試的事情,詳細可以上網 Google 一下,基於我對 PHPUnit 實在不熟,所以這邊就不獻寶了。

Mock with DI

Phalcon 有很多東西可以放在 DI 裡面,而,當我們要做 Functional Test 的時候,有些東西就必須要額外的傳入,舉個例子來說,

public function indexAction() {  
    // 我想要拿到 Request 的 get 傳入值
    $params = $this->request->get();
    return 'Succeeded!';
}

request 的來源是 \Phalcon\Http\Request 他原本就已經存在於 \Phalcon\Di\FactoryDefault() 裡面,所以本來是不需要額外設定。但是,因為你要跑的測試環境是在 CLI 底下,所以這個 request 就必須要用 Mock 來生一個替身並改寫,所以,理論上我們可以在 setUp() 裡面這樣做,

protected function setUp() {  
    $mock = $this->getMock('\\Phalcon\\Http\\Request', ['get']);
    $mock->expects($this->any())
        ->method('get')
        ->will($this->returnValue('Succeeded!'));
    $this->di->setShared('request', $mock);
}

那我們寫兩段測試,

public function test1TestCase() {  
    $response = $this->dispatch('/index');
    $this->assertEquals('Succeeded!', $response);
}

public function test2TestCase() {  
    $response = $this->dispatch('/index');
    $this->assertEquals('Succeeded!', $response);
}

最終執行結果會是,

Test results

疑?如果 Mock 有順利 Inject 到 $app 的話,理論上應該會拿到兩次都是成功的回應才對?為什麼第二次是 NULL 的結果?

接著我嘗試追蹤整個 setUp 的運作,他確實會把 Mock 順利注入 $app 裡面,但是當第一次成功之後,第二次的 setUp 注入卻拿不到任何返回值。

換句話說,mock 並沒有被運行。後來我將,

->will($this->returnValue('Succeeded!'));

置換成

->will($this->returnCallback(array($this, 'getMockReturnValue')));

之後,並且在 getMockReturnValue 埋入追蹤,發現一個現象,

  • 只要有任何一個 TestCase 通過,這個 Mock 就會被消滅。
  • 連帶的已經被埋入 DI 的 request 也會變回自己(而不是 替身
  • 反之測試要是 不通過,這個 Mock 就會繼續存活下去,直到有任何人通過測試。

這不是挺弔詭的嗎?

  1. 我每次 setUp 都重新設定 Mock 並且重新注入 DI
  2. 即便是把 \Phalcon\Http\Request 拿去外面重新 extends 再做 Mock 一樣失敗
  3. 是否是 PhalconPHP 哪裡做了什麼事情?
  4. 或是 PHPUnit Mock 不能這樣做?

以上問題我都不會,但是找到比較傳統的解法(欸

Functional Test 帶入自訂資料參數

首先,由於我們是要從 \Phalcon\Http\Request 拿資料,但是透過 Mock 又怪怪的,那只好用傳統的方式解決,

  1. 寫一個 Class 叫做 mockRequest
  2. extends \Phalcon\Http\Request
  3. 抄一下 Phalcon 原始碼關於 getJsonRawBody, get 的部分
  4. 如果你用到更多,請多抄點
  5. 抄完記得改
  6. 加一個方法 setRequest 讓我們可以傳自訂資料進來
  7. $this->di->setShared('request', new mockRequest());
  8. 在 Dispatch 的時候設定參數
  9. 收工

那個 Class 大概是長這樣,

class mockRequest extends \Phalcon\Http\Request  
{
    protected $mock_params = [];

    public function setRequest(array $params = []) {
        $this->mock_params = $params;
    }

    public function getJsonRawBody($convert = false) {
        if (true === $convert) {
            return $this->mock_params;
        }
        return json_encode($this->mock_params);
    }
    public function get($name = null, $filters = NULL, $defaultValue = NULL) {
        if (!is_null($name)) {
            if (isset($this->mock_params[$name])) {
                return $this->mock_params[$name];
            } else {
                return null;
            }
        }
        return $this->mock_params;
    }
}

然後在 dispatch 的時候把參數餵進去,

function dispatch($url, $params) {  
    $params = array_merge($params, [
        'key' => '你抓不到我',
        'cm' => 30
    ]);
    $request = $this->di->getShared('request');
    $request->setRequest($params);

    // ... 以下省略.
}

然後再跑一次測試,

Test result 2

然後就過了。
然後就過了。
然後就過了。

小結

我還是不會寫測試。

Hina Chen

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