Mock / Spy

模擬一段測試過程

測試過程中總會有一些流程必須跳過,像是寄信、打外部 API 等,略過這些流程的方法就是 Mock/Spy。


先舉一個例子,這是一個常見的類別,裡面有 __constructexecute 方法:

use App\Handler;

class Target
{
    public function __construct(private TemplateHandlerhandler $handler)
    {
    }
    
    public function execute($mailer): string
    {
        return $mailer->send($this->handler->getTemplate(), new User(3))
    }
}

這個類別相依了 Handler$mailerUser 的物件,在使用的時候會像這樣:

$handler = new TemplateHandler();
$target = new Target($handler);
$target->execute(new Mailer());

如果直接跑測試,就真的會寄信了。所以先示範一下如何 mock:

use Mockery;

/**
 * @test
 */
public function send_email_is_work(): void
{
    ## arrange
    // mock handler
    $handler = Mockery::mock(TemplateHandler::class);
    $handler->shouldReceive('getTemplate')
        ->once()
        ->andReturn('hello');
    // mock mailer
    $mailer = Mockery::mock(Mailer::class);
    $mailer->shouldReceive('send')
        ->once()
        ->andReturn('ok');
    ## act
    $result = (new Target($handler))->execute($mailer);
    ## assert
    $this->assertEquals('ok', $result);
}

以上述範例來說,handlermailer 是可以被 mock 的。但 new User(3) 無法被替換。這種 new class 的方式會造成功能的依賴耦合(Dependency),但 Laravel 有提供 app 可以隨時隨地的反轉依賴(Dependency inversion)。這種作法還有個好處,可以避免 __construct 寫太多東西:

app(User::class, ['id' => 3]);

如果要 mock Laravel 依賴注入這寫法,就必須要這樣寫:

// 物件沒有參數
app(TemplateHandler::class);
$this->mock(TemplateHandler::class);
// 物件有參數
app(User::class, ['id' => 3]);
$this->app->offsetSet(User::class, $this->mock(User::class));

Mock vs Spy

Mock 與 Spy 最大的差異是先後驗證,Mock 的驗證會寫在 arrange,Spy 驗證會寫在 assert 階段上。兩者的程式碼也差在 Spy 多了 shouldIgnoreMissing(),Spy 會在執行階段時忽略全部並回傳 null,如果 Spy 的物件需要回傳值就需要這樣寫:

## arrange
$spy = $this->spy(TemplateHandler::class);
$spy->expects()->getTemplate()->andReturn('hello')
## act
// do something...
## assert
$spy->shouldHaveReceived('execute')->once();

就如剛剛所說的,Spy 與 Mock 只差一個方法。所以 mock 也是可以使用 shouldHaveReceived,但 mock 通常會使用 shouldReceive 先行驗證,所以沒有必要驗證兩次,除非 mock 是使用 expects 來模擬行為。

如果希望所有驗證都寫在 assert 區塊,就使用 Spy。

我是覺得被 mock 的東西只是為了執行測試的手段過程,我就不會特別去寫驗證來證明有測過他,因為整個 I/O 都可能是假的。


不要 Mock/Spy

使用 Mock/Spy 可以更容易的寫好測試,但用得越多,整個測試就會越假,越偏離實際使用。像上面的範例,我可以把所以外部物件全都 mock 一遍,但就只能測到哪些物件有沒使用,而不能保護當中的物件如果遇到更改需求時能不能反應異常。

像是 TemplateHandlergetTemplate 本來是不需要參數的,但有天有個需要帶入第一個參數,這時測試還是會過。因為 mock/Spy 把整個物件取代掉了,所以不會知道這個物件的實際需求。

除非要做局部的測試(只測試目標的特定流程,故可以快速忽略前面的需求)或排除需要網路或外部服務的功能,否則不推薦濫用它們。

Last updated