每個程式都必定會去呼叫其他的函式,但我們在編寫測試時,也許有些函式不能隨意呼叫(e.g. 對外部送出 request),或者函式的執行時間較長(e.g. 存取檔案或外部資源)。
這時我們可以利用 Mock 的工具來幫助測試。
簡單來說,就是對原本要執行的函數或物件,做一個假貨、呼叫假的函式、回傳假的值等等,來模擬並取代它實際會做的事情。
讓我們拿 範例三 接著新增一些東西,假設我們要新增一個發薪日物件,而等待發薪水總是漫長的。
在範例三中,最長的測試執行時間也不超過0.1秒。
Mockery 是 PHP 常用的 Mock 套件,可以利用 Composer 安裝:
$ composer require mockery/mockery --dev
或者如果是下載範例程式碼,記得在 git checkout <tagname>
之後透過 composer.json
安裝:
$ composer install
會把所有需要的套件裝在 vendor/
中。
<?php
use PHPUnit\Framework\TestCase;
use Src\Payday;
class PaydayTest extends TestCase
{
public function testPaydayIsSlow()
{
$payday = new Payday();
$paid_result = $payday->pay();
$this->assertSame("paid", $paid_result[0]);
}
( 註:不需要 use Mockery )
<?php
namespace Src;
class Payday
{
public function pay()
{
// It will do something so slowly
sleep(2);
return ['paid', 'is', 'so', 'slow'];
}
}
我們故意在 pay()
中讓程式停止2秒,假裝這個函式要跑很多東西、執行了很久才回傳。
重新執行測試,當然也會發現因為執行了 pay()
而跑了兩秒多。
$ ./vendor/bin/phpunit
如果之後還有其他測試,都要呼叫到 pay()
,而我們並不需要每次都讓它完整執行,這時就可以用 Mock 物件來取代。
class PaydayTest extends TestCase
{
public function testPaydayByMock()
{
$mock_payday = Mockery::mock(Payday::class);
$mock_payday->shouldReceive('pay')
->once()
->andReturn(['paid', 'fast']);
$paid_result = $mock_payday->pay();
$this->assertSame("paid", $paid_result[0]);
Mockery::close();
}
}
( 範例程式碼:$ git checkout 3e
,包含以上兩種測試函式 )
利用Mockery::mock()
來回傳 Payday 的 mock 物件。
shouldReceive('pay')
是指參數內的這個函數 pay()
應該要被呼叫。
once()
是只呼叫一次,若不吻合則會跳出 InvalidCountException,也可以設定成 twice() 或 times(n)。
andReturn()
則是設定假的回傳值,接著我們對 mock_payday 物件做同樣的函式呼叫,就會模仿外觀行為,而不需要花費原本的執行時間。
記得最後面要加上 Mockery::close()
確保 mock 物件的關閉。
Mock 有助於解除,在測試時物件之間的相依性。但也請謹慎使用,它終究只是個 Mock 物件,並沒有真的運行,不小心可能會忽略掉某些測試狀況。
以及在覆蓋率的部分,用 Mock 的部分因為沒有執行原本產品程式的物件跟函式,並不算入覆蓋率的計算,因此會發現不是原本的100% 了。
Code Coverage Report:
Summary:
Classes: 66.67% (2/3)
Methods: 75.00% (3/4)