之前已有一篇文章介紹如何使用 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());
}
}



