在開發程式時, 寫單元測試可以幫助我們之後在程式更動時能更有把握, 對於未來上可以減少一些維護上的風險與成本, 各程式語言都有各自的工具或框架進行測試, 本篇將介紹 PHP 基本的測試工具 PHPUnit 設定與基本的 mock 方法。
基本資料
官網:https://phpunit.de/
原始碼:https://github.com/sebastianbergmann/phpunit
文件 (英文):https://phpunit.readthedocs.io/
文件 (中文):https://phpunit.readthedocs.io/zh_CN/latest/
安裝與設定 PHPUnit
安裝 phpunit
首先在專案中使用 composer 安裝 PHPUnit
composer require --dev phpunit/phpunit
安裝 php-xdebug
如果要能夠看到測試的 Code Coverage 就還需要安裝 php-xdebug
pecl install xdebug
設定 phpunit.xml
安裝完畢需要在專案目錄建立 phpunit 設定檔 phpunit.xml
<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false"> <testsuites> <testsuite name="xenby-project"> <directory suffix="Test.php">tests</directory> </testsuite> </testsuites> <logging> <log type="coverage-text" target="php://stdout" /> </logging> <filter> <whitelist> <directory suffix=".php">src</directory> </whitelist> </filter> </phpunit>
上面這個範例中為, 分別設定為:
- backupGlobals:每此測試執行時時會先備份 $GLOBALS, 而測試結束會將 $GLOBALS 恢復, 使得每個測試之間的 $GLOBALS 內容不會互相影響
- backupStaticAttributes:每此測試執行時時會先備份以物件的 static 屬性的值, 而測試結束會將恢復, 使得每個測試之間的 static 屬性不會互相影響
- colors:在 termimal 顯示的文字增加顏色
- convertErrorsToExceptions:執行過程中發生 Error 時會以 Exception 方式丟出
- convertNoticesToExceptions:執行過程中發生 Notice 錯誤時會以 Exception 方式丟出
- convertWarningsToExceptions:執行過程中發生 Warning 時會以 Exception 方式丟出
- processIsolation 每次的測試都使用獨立的 process 進行運作 (可達到每個都完全獨立, 不過會讓測試執行較慢)
- stopOnFailure 當有任何錯誤時就停止, 不繼續執行其他測試
- testsuites 中的 suffix 與 directory 為設定要掃描哪個資料夾下面什麼檔案名結尾的檔案進行測試, 以這邊範例是 tests 資料夾下 Test.php 的檔案
撰寫測試
基本測試與執行
這時就可以開始寫測試了, 只需要在 tests 資料夾下面建立 Test.php 結尾的檔案, 並且該 class 繼承 PHPUnit\Framework\TestCase
就會是一個測試檔案
例如建立一個 DemoTest.php 裡面的程式碼為:
<?php namespace Xenby\Project\Test; use PHPUnit\Framework\TestCase; class DemoTest extends TestCase { public function testBase() { $this->assertEquals(2, 1 + 1); } }
這時只需要輸入指令執行測試就可以了
./vendor/bin/phpunit
可以看到類似如下的結果
PHPUnit 9.5.10 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 00:00.152, Memory: 12.00 MB OK (1 tests, 1 assertions)
這樣就可以開始寫基本測試了
使用 Mock Builder 替換部分方法
有時可能會遇到有些函數或功能無法在測試中實際去執行的功能, 例如使用第三方的付費功能或是開發票功能時, 這時可以使用 PHPUnit 提供的 Mock Builder 功能
此功能能夠替換掉物件部分函數, 使其在測試時不會實際執行函數內的功能, 並且產生自己指定要的結果
以實際程式碼做示範, 假如我們寫了一個 InvoiceClient 用來呼叫第三的 API 建立發票函數 createNewInvoice
class InvoiceClient { /** * 請第三方建立發票 * * @param int $order_id 訂單編號 * @return int 收據編號 * @throws Exception 建立失敗 */ public function createNewInvoice($order_id) { // ...(實際的實作) } }
而這時我們可以使用 MockBuilder 產生一個假的 InvoiceClient, 其 createNewInvoice 函數功能是被替換的
接著便可以把 $mockInvoiceClient 傳進其他物件中, 給其他物件呼叫並測試
use PHPUnit\Framework\TestCase; class InvoiceClientTest extends TestCase { public function testCreateNewInvoice() { $mockInvoiceClient = $this->getMockBuilder(InvoiceClient::class) ->onlyMethods(["createNewInvoice"]) // 指定哪些函數會被 mock 掉 ->disableOriginalConstructor() // 建立此物件時不呼叫建構元 ->getMock(); $orderId = 5824; $mockInvoiceId = 4520312; $mockInvoiceClient->method('createNewInvoice') ->with($this->equalTo($orderId)) // 預期呼叫此函數收到的參數 ->will($this->returnValue(true)); // 此函數 return 的值 $this->assertEquals($mockInvoiceId, $mockInvoiceClient->createNewInvoice($orderId)); } }
Mock Builder 不只能夠讓函數回傳資料, 也可以讓函數丟出例外, 用來測試失敗的情境
use PHPUnit\Framework\TestCase; class InvoiceClientTest extends TestCase { public function testCreateNewInvoiceFailed() { $mockInvoiceClient = $this->getMockBuilder(InvoiceClient::class) ->onlyMethods(["createNewInvoice"]) // 指定哪些函數會被 mock 掉 ->disableOriginalConstructor() // 建立此物件時不呼叫建構元 ->getMock(); $orderId = 5824; $mockException = new Exception('Create Invoce Failed'); $mockInvoiceClient->method('createNewInvoice') ->with($this->equalTo($orderId)) // 預期呼叫此函數收到的參數 ->willThrowException($mockException); // 此函數丟出的例外 $this->expectException(Exception::class); $this->expectExceptionMessage($mockException->getMessage()); $mockInvoiceClient->createNewInvoice($orderId); } }