[PhalconPHP] 棄 PHPunit 投入 Codeception

請叫我踩雷王!


PHPUnit 與 Phalcon DI 的困擾

特別提出 DI 是因為資料庫的關係,由於自己比較機歪,把 Functional Testing 從 handle 的入口開始做,這樣變成測試有點繁瑣,

  1. 會先進入 Application 初始化
  2. 設定 DI
  3. 開始進入 Router
  4. 進入 Router 後會先跑一遍 Middleware
  5. 通過之後才進入 Controller
  6. 從 Controller 拿到回應之後,再進入後續 Middleware
  7. 最後返回相關的回應

handle 開始跑的原因就是,我想要測試的方式是一條龍,把 Micro Application,也就是 REST API 的方法從開始到結束測試一遍。

問題來了,DI 老是出狀況,自從上次的 DI 被重設事件之後,接著是 Mock,而這次演變到 ModelManager 出狀況。由於需要跟資料庫連線,所以通常會在 Controller 打開交易模式,

// 開始交易
$transaction = $this->transactionManager->get();

// ... 中間省略一萬行

// 結束交易並提交
$transaction->commit();

// 或者是回滾
$transaction->rollback();

這樣看起來沒有問題,實際操作前端也沒有問題。

只要是分開的步驟做都沒有問題。

問題在,當我把這些 TestCase 全部放在一起跑的時候,我的交易狀態就莫名的被中斷了。也就是說,我跑了 10 個測試當中,有機會會有一兩個出現這個狀況,

- There is no active transaction.

本以為是哪裡寫錯,後來隨意調換 TestCase 的順序時,發現交易狀態會在不同的地方被中斷。

所以推測,PHPUnit 在每次 TestCase 跑的時候,執行狀態結束後,資源回收的不乾淨,導致 DI 的東西被弄髒了(只是個人推測,歡迎提出相關討論

又由於 $transaction = $this->transactionManager->get(); 其實在 DI 當中是等同於,

$this->db->begin();

// ... 中間省略一萬行

// 結束交易並提交
$this->db->commit();

// 或者是回滾
$this->db->rollback();

這個 $this->db 其實就雷同於,

$di = $application->getDI();
$db = $di->getService('db');
// 或是
$db = $di->get('db');
// 或是,如果你當初設定為 setShared,或是 set 第三參數給 true
$db = $di->getShared('db');

然後就因為他其實是資料庫連線層的那一塊(不知道該怎麼解釋比較妥當?

總之就是,你每次的交易都只能被 begin 一次,直到結束 commitrollback 之間,都不能再次 begin,不然會噴錯

但是 Phalcon 也貼心的幫你避開重複 begin 的錯誤,結果就會導致,有兩個地方 commit 或是 rollback 的時候,第二順位的就會出現 There is no active transaction. 的錯誤。

問題就出在這裡,不知為何 PHPUnit 在我把所有 TestCase 放在一起執行時,有人被中斷了。所以依照這個邏輯來看,假設我有 A, B, C 三組測試,

  • A1 新增資料,接著 commit
  • B1 新增資料,接著 commit
  • B2 讀取資料,更新資料,接著 commit
  • C1 新增資料,接著 commit

然後檔案的順序就是利用 phpunit.xml 來固定他們的順序,

<file>ATest.php</file>
<file>BTest.php</file>
<file>CTest.php</file>

接著開始跑,然後報錯,錯誤發生在 C1 的 commit 出現 There is no active transaction.

然後,原本以為是 C1 這個測試案例寫錯,好,註解掉 C.php,再跑一次,結果錯誤發生在 B2 身上,好,註解掉 B2 這個案例,再跑一次,

然後就過了(疑?

接著把 C1 這個測試案例加回來,再跑一次,

然後就過了(疑?

接這把 B2 加回來,並且把 C1 拿掉,再跑一次,

然後就過了(疑?

WTF

最後把 B2 放去 A.php 然後再跑一次,

C1 commit - There is no active transaction.

所以我說我不會寫測試,你現在相信了吧。


Codeception

經過好友 Steve Lo 的推薦,改用 Codeception 來做測試的事情。看了一下官方的介紹,有支援 Phalcon 不錯,還可以針對 Angular 測試,也可以 BDD 真是太神啦!

然後我就送了個關於 Phalcon2 的 PR

請叫我踩雷王

然後官方已經收了

https://github.com/Codeception/Codeception/pull/3172

MVC / Micro Application 注意事項

要用他們的 Phalcon 有幾個地方要注意,

  1. 測試的返回值需要是 \Phalcon\Di\Injectable
  2. Micro Application Middleware 會噴錯我已經送 PR
  3. Micro Application 不要任意 $app->stop() 或是 $app->response->send()

關於第 1 點,你要改你的 $application->handle()

// APP_ENV 不要照抄謝謝

// 假設你是 MVC Application
if (getenv('APP_ENV') === 'testing') {
    return $application;
} else {
    echo $application->handle();
}

// 假設你是 Micro Application
if (getenv('APP_ENV') === 'testing') {
    return $application;
} else {
    $application->handle();
}

關於第 3 點,如果你的 Middleware 需要中斷執行,請避開 $app->stop() 或是 $app->response->send()

$app->before(function() use ($app) {
    $middleware = new Middleware();

    if (false === $middleware->call($app)) {
        if (getenv('APP_ENV') !== 'testing') {
            $app->stop();
        }

        $app->response->setStatusCode(400)->sendHeaders();
        $app->response->setJsonContent([]);

        if (getenv('APP_ENV') !== 'testing') {
            $app->response->send();
        }

        // 這裡的 return false 會造成第 2 點的錯誤
        return false;
    }

    return true;
});

至於第 2 點,如果你等不及他們 Merge 發更新,請去這裡找,

vendor/codeception/codeception/src/Codeception/Lib/Connector/Phalcon.php

大概是 155 行

$response = $application->handle();

在底下加入,

if (!$response instanceof Phalcon\Http\ResponseInterface) {
    $response = $application->response;
}

這樣就差不多了,然後就可以快快樂樂寫測試了。

小結

Steve Lo 惠我良多

什麼時候要一起吃飯 XD

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