在開發程式時, 寫單元測試可以幫助我們之後在程式更動時能更有把握, 對於未來上可以減少一些維護上的風險與成本, 各程式語言都有各自的工具或框架進行測試, 本篇將介紹 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);
}
}



