PHP CodeIgniter 3 單元測試日常:建立 PHPUnit 測試環境

本文將帶領讀者建立 CodeIgniter 3 框架的 PHPUnit 測試環境,給 CodeIgniter 3 一個現代化的機會!
本文假設您已經具備 軟體測試自動化 以及 PHPUnit 相關知識,且了解如何撰寫測試案例。若您尚未了解單元測試或軟體測試自動化,這裡提供一些不錯的資源讓您初步了解:
除了上面兩個資源外,請您務必花時間認識並學習軟體測試,這可以說是軟體開發技術的核心技能之一。
若您尚未了解 PHPUnit 這裡也有些簡單的文件供您參考:

前言

2020 年對於 PHP 界風靡一時的 MVC 框架「CodeIgniter」來說,光環已經被新星 Laravel 搶去。雖然 CodeIgniter 的討論熱度已經消退了,但市佔率仍然相當高,至今仍有許多 PHPer 還在與 CodeIgniter 奮鬥和成長(包括我)。
由於 CodeIgniter 3 框架(以下簡稱 CI3)沒有使用 Namespace 的特性,加上 CI3 統一透過框架內建的 Loader 類別實現 Autoload 機制,造成很多 PHPer 沒辦法在 CI3 框架中使用現代 PHP 的特性來開發系統。若不做點手腳的話,PHPer 的開發思維很容易就會被 CI3 框架綁架,一不小心就將所有業務邏輯全部寫在 CI3 框架中。換句話說所有的程式碼都依賴於框架,物件導向 SOLID 原則的實現、設計模式、單元測試 …等較進階的管理程式碼策略都不用談了!
為了讓還在與 CI3 奮鬥的同袍們能夠使用現代 PHP 的特性來開發系統,本文將介紹如何在 CI3 框架中建立單元測試的環境,讓 CI3 也能使用並測試現代 PHP 的程式碼,確保 PHPer 能安心地實現各種開發策略和思維。

導入 Composer 擁抱現代 PHP 特性

其實 CI3 跟現代 PHP 只差臨門一腳,你可以在 config/Config.php 中找到一個設定為 composer_autoload,只要替 composer_autoload 設定 Composer 目錄底下的 autoload.php 路徑,你的 CI3 框架就可以開始使用 Namespace 搭配 PSR-4 Autoload 機制來開發現代 PHP 特性的程式碼!
編輯 application/config/config.php:
/*
|--------------------------------------------------------------------------
| Composer auto-loading
|--------------------------------------------------------------------------
|
| Enabling this setting will tell CodeIgniter to look for a Composer
| package auto-loader script in application/vendor/autoload.php.
|
|    $config['composer_autoload'] = TRUE;
|
| Or if you have your vendor/ directory located somewhere else, you
| can opt to set a specific path as well:
|
|    $config['composer_autoload'] = '/path/to/vendor/autoload.php';
|
| For more information about Composer, please visit http://getcomposer.org/
|
| Note: This will NOT disable or override the CodeIgniter-specific
|    autoloading (application/config/autoload.php)
*/
- $config['composer_autoload'] = FALSE;
+ $config['composer_autoload'] = __DIR__ . '/../../vendor/autoload.php'; // Composer 的 autoload.php 路徑

安裝 ci-phpunit-test

由於 CI3 架構內建的單元測試功能很少,所以我選用整合了 PHPUnit 的 ci-phpunit-test 來當作專案的測試框架。
ci-phpunit-test 除了包含 PHPUnit 測試框架本身的功能以外,還提供模擬 HTTP Request 的整合測試功能,重構前建立回歸測試來說相當方便!不過使用這個套件之前,有一些必要條件:
  • PHP 版本至少高於 5.4.0
  • CodeIgniter 至少要有 3.0
  • 至少要安裝 PHPUnit 4.3 以上的版本

透過 Composer 下載 ci-phpunit-test

到你的專案目錄下使用 Composer 下載 ci-phpunit-test
$ cd /path/to/codeigniter/
$ composer require kenjis/ci-phpunit-test --dev

透過 install.php 安裝 ci-phpunit-test

下載完成後,需要執行一次 install.php
注意,必須要在專案的根目錄執行安裝指令
安裝指令每次都會建立並覆蓋整個 application/tests 目錄
$ cd /path/to/codeigniter/
$ php vendor/kenjis/ci-phpunit-test/install.php
安裝完成後,application 目錄下會出現 tests 目錄,以後測試程式都要放在這個 tests 目錄底下:
codeigniter/
├── application/
│   └── tests/
│        ├── _ci_phpunit_test/ ... 裡面是 ci-phpunit-test 框架的原始碼
│        ├── Bootstrap.php     ... PHPUnit 的 Bootstrap 設定檔
│        ├── DbTestCase.php    ... 
│        ├── TestCase.php      ... 測試案例的基底類別
│        ├── controllers/      ... 把你的 Controller 測試程式放進來(整合測試)
│        ├── libraries/        ... 把你的 Library 測試程式放進來(整合測試或單元測試)
│        ├── mocks/
│        │   └── libraries/    ... 把 Mock 模擬物件放進來
│        ├── models/           ... 把你的 Model 層測試程式放進來
│        └── phpunit.xml       ... PHPUnit 的設定檔
└── vendor/

第一次執行 ci-phpunit-test

在執行 ci-phpunit-test 之前,一定要確保兩件事:
(以下為 OSX 系統安裝 PHPUnit 步驟)
$  wget https://phar.phpunit.de/phpunit-7.0.phar
$  chmod +x phpunit-7.0.phar
$  sudo mv phpunit-7.0.phar /usr/local/bin/phpunit
$  phpunit --version
PHPUnit x.y.z by Sebastian Bergmann and contributors.
這麼做是因為 ci-phpunit-test 要求一定要在 application/tests 目錄執行 phpunit
$ cd /path/to/codeigniter/
$ cd application/tests/
$ phpunit
PHPUnit 7.5.18 by Sebastian Bergmann and contributors.

...
Time: 341 ms, Memory: 5.50Mb

撰寫測試案例:

到目前為止,你的 CI3 框架已經完全整合了 PHPUnit,並且可以利用 Composer 的 Autoload 機制引入使用現代 PHP 特性的受測程式碼。但是本篇主題是建立測試環境,若還想知道更多測試案例的寫法,可以直接參考 ci-phpunit-test 作者提供的線上手冊:How to Write Tests 和電子書 CodeIgniter Testing Guide
我也會陸續新增幾篇 CodeIgniter 撰寫整合測試、單元測試的文章供大家參考。

CI3 + HMVC 測試環境配置

因為 CodeIgniter + HMVC 的結構較為特殊,安裝 ci-phpunit-test 之後還需手動做一些修改,才能在 HMVC 的環境中執行ci-phpunit-test 測試框架。
接下來的修改是參考 ci-phpunit-test 作者針對 Github issue 34:Any luck with ci-phpunit-test working with HMVC? 的回應,對 CI3 做調整。這些調整的目的是改變 CI3 載入 HMVC 套件的順序,讓測試環境取得 HMVC 的物件,而不是原生 CI 的物件。

步驟一:修改 Modules.php

編輯 ci/application/third_party/MX/Modules.php:
<?php (defined('BASEPATH')) OR exit('No direct script access allowed');

(defined('EXT')) OR define('EXT', '.php');

global $CFG;

+if (ENVIRONMENT === 'testing')
+{
+   $CFG =& load_class('Config');
+}

步驟二:修改 Loader.php

編輯 ci/application/third_party/MX/Loader.php:
public function initialize($controller = NULL)
{
+    if (ENVIRONMENT === 'testing')
+    {
+        // Rest CI::$APP
+        CI::$APP = CI_Controller::get_instance();
+        if ( ! CI::$APP->lang instanceof MX_Lang) CI::$APP->lang = new MX_Lang;
+    }

    /* set the module name */
    $this->_module = CI::$APP->router->fetch_module();

步驟三:設定 PHPUnit 預設設定檔案

在執行 ci-phpunit-test 之前,記得要將 PHPUnit 的設定檔案預設為:
ci/application/tests/phpunit.xml
否則 ci-phpunit-test 會因為沒載入 Autoload 而無法順利執行。

注意事項

禁止使用 header() 跳頁

整合測試中,ci-phpunit-test 只接受 CI3 的 Output 類別輸出結果,若在程式運行中途對 header 設定做任何修改,測試框架將會出現錯誤訊息。為了讓測試案例可以運作,應盡量在系統中使用 CI3 內建的 redirect 進行轉址,若沒辦法避免使用 header(),也可以用 ENVIRONMENT 環境變數進行跳脫。
-header('Location: ' . base_url(). 'admin');
+if (ENVIRONMENT === 'testing') {
+    redirect(base_url(). 'admin');
+} else {
+    header('Location: ' . base_url(). 'admin');
+}

推薦安裝:Codeception\Specify

Codeception\Specify 供測試環境使用 BDD 語法。
Codeception\Specify 可以在測試案例中隔離測試情境,減少很多因為情境產生的測試 function。
透過 Composer 安裝 Codeception\Specify
$ composer require codeception/specify
使用方法:直接在測試程式中引入 Codeception\Specify Trait:
<?php
class UserTest extends PHPUnit\Framework\TestCase 
{
+    use Codeception\Specify;
使用範例:簡化多個測試案例情境
簡化多個測試案例情境,並以中文描述情境

結尾

這篇文章其實「CodeIgniter 3 單元測試日常」系列文的揭幕,這系列文的目的除了分享經驗以外,也想讓更多仍在維護 CodeIgniter 框架(或非現代 PHP 框架)的開發人員知道,原來手頭上的框架也可以變得現代化!希望可以給維護舊框架的開發人員帶來一絲希望,不要被舊框架或專案嚇跑了!

留言

  1. 很棒的文章,BTW, CI3 好像只能跑 phpunit 8.x,9.x 會出錯喔!

    回覆刪除

張貼留言

這個網誌中的熱門文章

Git Commit Message 這樣寫會更好,替專案引入規範與範例

Gitlab 合併請求 Merge Request 是什麼?

PHP OO 物件導向基礎教學