之前已有一篇文章介紹如何使用 PHPUnit 的 Mock Builder 替換物件的一些函數, 不過使用時會注意到該方法只能用於實體 (new 出來的 object), 而如果是 static 就不能夠替換掉, 且該方式也只適用 object 是透過從外部注入的寫法, 如果是在 function 內直接 new class 方式就無法使用, 這邊介紹一個套件 AspectMock 可以解決以上兩個問題。
介紹
Aspect 是使用 goaop/framework 來實作 mock 功能, 其有以下幾個特點
- 支援 mock static 函數
- 可以隔空 (on the fly) 替換函數, 不需要先建立實體或對實體進行操作
- 支援 mock PHP 內建的函數 (例如: 取現在的時間
date('Y-m-d H:i:s')
)
安裝與設定
在使用 AspectMock 之前先確定已安裝 PHPUnit, 還沒安裝可以參考之前的文章
有安裝 PHPUnit 之後, 接著透過 composer 安裝 AspectMock
composer require --dev codeception/aspect-mock
並且建立 tests/bootstrap.php
設定 AspectMock
<?php include __DIR__ . '/../vendor/autoload.php'; $kernel = \AspectMock\Kernel::getInstance(); $kernel->init([ 'debug' => true, 'includePaths' => [__DIR__ . '/../src'], // 要 mock 的 class 原始碼存放位置 (也可以寫 vendor 底下的套件) 'cacheDir' => '/tmp/xenby-AspectMock-tmp', // Cache 存放位置 ]);
並且設定 phpunit.xml 讓 PHPUnit 去讀取此 bootstrap.php
(phpunit DOM 上的 bootstrap)
<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" bootstrap="tests/bootstrap.php" 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>
開始撰寫測試
接著就可以開始撰寫測試, 下面示範一些 AspectMock 使用範例
使用 double 函數進行基本的函數 mock
原始碼
class InvoiceClient { /** * 請第三方建立發票 * * @param int $order_id 訂單編號 * @return int 收據編號 * @throws Exception 建立失敗 */ public function createNewInvoice($order_id) { // ...(實際的實作) } }
測試程式碼, 使用 AspectMock 的 double 函數可以直接隔空取代掉函數功能,回傳 mock 的值
use AspectMock\Test as AspectMockTest; use PHPUnit\Framework\TestCase; class InvoiceClientTest extends TestCase { public function testCreateNewInvoice() { $mockInvoiceId = 1854; AspectMockTest::double(InvoiceClient::class, ['createNewInvoice' => $mockInvoiceId]); $client = new InvoiceClient(); $orderId = 193; $this->assertEquals($mockInvoiceId, $client->createNewInvoice($orderId)); } }
使用 double 函數進行 mock 使函數丟出例外
原始碼
class InvoiceClient { /** * 請第三方建立發票 * * @param int $order_id 訂單編號 * @return int 收據編號 * @throws Exception 建立失敗 */ public function createNewInvoice($order_id) { // ...(實際的實作) } }
使用 double 函數為指定函數替換實際功能
use AspectMock\Test as AspectMockTest; use Exception; use PHPUnit\Framework\TestCase; class InvoiceClientTest extends TestCase { public function testCreateNewInvoiceFailed() { $mockException = new Exception('Create Invoce Failed'); AspectMockTest::double(InvoiceClient::class, [ 'createNewInvoice' => function () use ($mockException) { throw $mockException; }, ]); $this->expectException(Exception::class); $this->expectExceptionMessage($mockException->getMessage()); $client = new InvoiceClient(); $orderId = 5824; $client->createNewInvoice($orderId); } }
使用 double 函數替換掉 static 函數
原始碼
class InvoiceClient { /** * 取得發票伺服器狀態 * * @return array [ * 'status' => status id, * 'message' => message * ] */ public static function getServerStatus() { // ...(實際的實作) } }
測試程式碼
use AspectMock\Test as AspectMockTest; use PHPUnit\Framework\TestCase; class InvoiceClientTest extends TestCase { public function testGetServerStatus() { AspectMockTest::double(InvoiceClient::class, [ 'getServerStatus' => [ 'status' => 0, 'message' => 'OK', ], ]); $this->assertEquals([ 'status' => 0, 'message' => 'OK', ], InvoiceClient::getServerStatus()); } }
使用 func 函數替換掉 PHP 內建函數
原始碼
class InvoiceClient { /** * 取得在時間 * * @return string */ public static function getNowTime() { return date('Y-m-d H:i:s'); } }
測試程式碼 (實際是在該 namespace 下注入同名函數)
use AspectMock\Test as AspectMockTest; use PHPUnit\Framework\TestCase; use ReflectionClass; use Xenby\Project\Clients\InvoiceClient; class InvoiceClientTest extends TestCase { public function testGetNowTime() { $class = new ReflectionClass(InvoiceClient::class); AspectMockTest::func($class->getNamespaceName(), 'date', '2020-04-03'); $this->assertEquals('2020-04-03', InvoiceClient::getNowTime()); } }