Laravel — Testing — Mocking (官方文件原子化翻譯)
By: Ray Lee
Tags: Laravel, Mock, Documentation, Backend

Photo by Brett Jordan on Unsplash

# 版本

Laravel 8.x

# 前言

我喜歡使用 Laravel 開發的感覺, 除了開發快速, 程式碼簡潔且優雅之外, Laravel 框架本身也是一個很好的學習參照物。 本篇主要將官方文件重點整理成 Q&A 的形式呈現, 原子化的概念, 這方式並不適用於每個人, 但若對你有幫助, 我會很開心。

# 目錄

Laravel — 官方文件原子化翻譯 — 目錄

# Mocking Objects

以下的 Laravel example code 的意思是？

Example:

<?php

use App\Service;

use Mockery;

use Mockery\MockInterface;



public function test_something_can_be_mocked()

{

$this->instance(

Service::class,

Mockery::mock(Service::class, function (MockInterface $mock) {

$mock->shouldReceive('process')->once();

})

);

}

Answer: 將 service::class 綁到 service container 當中, 當完成綁定後, service container 將會使用 mocked class, 而不是原本的 object, 並指定 process method 會被呼叫一次

以下的 Laravel example code 的意思是？

Example:

<?php

use App\Service;

use Mockery\MockInterface;



$mock = $this->mock(Service::class, function (MockInterface $mock) {

$mock->shouldReceive('process')->once();

});

Answer: 將 service::class 綁到 service container 當中, 當完成綁定後, service container 將會使用 mocked class, 而不是原本的 object, 並指定 process method 會被呼叫一次

以下的 Laravel example code 的意思是？

Example:

<?php

use App\Service;

use Mockery\MockInterface;



$mock = $this->partialMock(Service::class, function (MockInterface $mock) {

$mock->shouldReceive('process')->once();

});

Answer: 使用 partialMock() 將 Service::class 綁定 container, 所以 container 會使用 mocked class, 而不是實際上的 object, 並使用 shouldReceive() 宣告 process method 會被呼叫, once() 定義次數 partialMock 只 mock 被呼叫的 method, 其他 method 如果在 testing 過程中有被呼叫, 則正常執行

以下的 Laravel example code 的意思是？

Example:

<?php



namespace Tests\Feature;



use Illuminate\Foundation\Testing\RefreshDatabase;

use Illuminate\Foundation\Testing\WithoutMiddleware;

use Illuminate\Support\Facades\Cache;

use Tests\TestCase;



class UserControllerTest extends TestCase

{

public function testGetIndex()

{

Cache::shouldReceive('get')

->once()

->with('key')

->andReturn('value');



$response = $this->get('/users');



// ...

}

}

Answer: mock Cache facade, 並使用 shouldReceive 斷言 get() 將被呼叫, once() 代表次數, with() 代表 parameter, 而 return value 代表回傳的值, 如果沒有達到以上的斷言, 則 fail

Laravel testing 中, 如果要 mock config, 該使用？

config::set

Laravel testing 中, 如果要 mock http testing, 該使用？

http testing method

# Facade Spies

以下的 Laravel example code 的意思是？

Example:

<?php

use Illuminate\Support\Facades\Cache;



public function test_values_are_be_stored_in_cache()

{

Cache::spy();



$response = $this->get('/');



$response->assertStatus(200);



Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);

}

Answer: 使用 spy() method 來紀錄下所有 testing 過程與 Cache facade 的互動, 並在最後 assert, 若不符合 assertion 則報錯

# Bus Fake

以下的 Laravel example code 的意思是？

Example:

<?php



namespace Tests\Feature;



class ExampleTest extends TestCase

{

public function test_orders_can_be_shipped()

{

Bus::fake();



// Perform order shipping...



Bus::assertDispatched(ShipOrder::class);



Bus::assertNotDispatched(AnotherJob::class);

}

}

Answer: 使用 Bus facade 的 fake(), 任何使用到 Bus facade dispatch 的 job 將不會真正的被 dispatch 最後可使用 assertDispatched(), assertNotDispatched() 來斷言哪個 job 被 dispatch 了, 哪個沒有

以下的 Laravel testing example code 的意思是？

Example:

<?php

Bus::assertDispatched(function (ShipOrder $job) use ($order) {

return $job->order->id === $order->id;

});

Answer: 當使用 Bus facade 的 assertDispatched(), assertNotDispatched() 時, 可帶入 closure 來判定被 dispatched 的 job 有通過一定的規則

# Job Chains

以下的 Laravel example code 的意思是？

Example:

<?php

use App\Jobs\RecordShipment;

use App\Jobs\ShipOrder;

use App\Jobs\UpdateInventory;

use Illuminate\Support\Facades\Bus;



Bus::assertChained([

ShipOrder::class,

RecordShipment::class,

UpdateInventory::class

]);

Answer: assert 指定的 job 有被 chained 且 dispatched

以下的 Laravel example code 的意思是？

Example:

<?php

Bus::assertChained([

new ShipOrder,

new RecordShipment,

new UpdateInventory,

]);

Answer: assert 指定的 job 有被 chained 且 dispatched, 除了可帶入 class name, 也可帶入 class instance

# Job Batches

以下的 Laravel example code 的意思是？

Example:

<?php

use Illuminate\Bus\PendingBatch;

use Illuminate\Support\Facades\Bus;



Bus::assertBatched(function (PendingBatch $batch) {

return $batch->name == 'import-csv' &&

$batch->jobs->count() === 10;

});

Answer: 使用 assertBatched() 來斷言 batch of jobs 已被 dispatched, 可在 closure 內取得 PendingBatch, 取得該 batch 的資料, 並定義該 batch 應該要有的條件

# Event Fake

以下的 Laravel example code 的意思是？

Example:

<?php



class ExampleTest extends TestCase

{

public function test_orders_can_be_shipped()

{

Event::fake();



// Perform order shipping...



Event::assertDispatched(OrderShipped::class);



Event::assertDispatched(OrderShipped::class, 2);



Event::assertNotDispatched(OrderFailedToShip::class);



Event::assertNothingDispatched();

}

}

Answer: 使用 fake(), 則該 event 的 listener 將不會真正的 dispatch assertDispatched() 斷言指定的 event 需被 dispatched assertDispatched() arg2 代表該 event 需被 dispatched 2 次 assertNotDispatched 斷言該 event 沒被 dispatched assertNothingDispatched 斷言沒有任何 event 被 dispatched

以下的 Laravel testing example code 的意思是？

Example:

<?php

Event::assertDispatched(function (OrderShipped $event) use ($order) {

return $event->order->id === $order->id;

});

Answer: 使用 closure 來斷言, 符合定義條件的 job 有被 dispatched

以下的 Laravel testing example code 的意思是？

Example:

<?php

Event::assertListening(

OrderShipped::class,

[SendShipmentNotification::class, 'handle']

);

Answer: 斷言指定的 listener 有 listen 定義的 event

以下的 Laravel testing 中, 如果有使用到 Factory 的 model event, Event::fake() 需使用在 Factory 之後, 原因為何？

因為一旦使用了 Event::fake(), 所有 event 都不會被執行

# Faking A Subset Of Events

以下的 Laravel testing example code 的意思是？

Example:

<?php

public function test_orders_can_be_processed()

{

Event::fake([

OrderCreated::class,

]);



$order = Order::factory()->create();



Event::assertDispatched(OrderCreated::class);



// Other events are dispatched as normal...

$order->update([...]);

}

Answer: 可帶入 class 到 Event::fake(), 這樣只有該 event 會被 fake, 其餘的 event 照常執行

Scoped Event Fakes

以下的 Laravel testing example code 的意思是？

Example:

<?php



class ExampleTest extends TestCase

{

public function test_orders_can_be_processed()

{

$order = Event::fakeFor(function () {

$order = Order::factory()->create();



Event::assertDispatched(OrderCreated::class);



return $order;

});



$order->update([...]);

}

}

Answer: 只有 fakeFor() 內, closure 範圍內的 event 才會被 fake, 其餘的照常執行

# HTTP Fake

可參考 Fake HTTP Client

# Mail Fake

以下的 Laravel testing example code 的意思是？

Example:

<?php

public function test_orders_can_be_shipped()

{

Mail::fake();



Mail::assertNothingSent();



Mail::assertSent(OrderShipped::class);



Mail::assertSent(OrderShipped::class, 2);



Mail::assertNotSent(AnotherMailable::class);

}

Answer: 使用 Mail::fake(), 模擬 mail 寄出 testing, 實際上不會真的寄出 assert 沒有任何 mailable 被送出 assert 指定的 mailable 被發送 assert 指定的 mailable 被發送兩次 assert 指定的 mailable 沒有被送出

以下的 Laravel testing example code 的意思是？

Example:

<?php

Mail::assertQueued(OrderShipped::class);



Mail::assertNotQueued(OrderShipped::class);



Mail::assertNothingQueued();

Answer: 當要測試用 queue 發送的 mailable 時, 使用 assertQueued() assert OrderShipped mailable 由 queue 發送 assert OrderShipped mailable 沒有由 queue 發送 assert 沒有任何 mailable 經由 queue 被發送

以下的 Laravel testing example code 的意思是？

Example:

<?php

Mail::assertSent(function (OrderShipped $mail) use ($order) {

return $mail->order->id === $order->id;

});

Answer: assertSent 以及 assertNotSent 也可帶入 closure, 判斷是否有通過 closure 內條件的 mailable 被發送

以下的 Laravel testing example code 的意思是？

Example:

<?php

Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {

return $mail->hasTo($user->email) &&

$mail->hasCc('...') &&

$mail->hasBcc('...');

});

Answer: 除了 assert 指定 mailable 被送出外, 還可以 assert 寄給誰 cc 給誰 bcc 給誰

# Notification Fake

以下的 Laravel testing example code 的意思是？

Example:

<?php

public function test_orders_can_be_shipped()

{

Notification::fake();



// Perform order shipping...



Notification::assertNothingSent();



Notification::assertSentTo(

[$user], OrderShipped::class

);



Notification::assertNotSentTo(

[$user], AnotherNotification::class

);

}

Answer: 使用 Notification::fake() 斷言沒有任何 notification sent 斷言指定的 notification sent to a given user 斷言指定的 notification not sent to a given user

以下的 Laravel testing example code 的意思是？

Example:

<?php

Notification::assertSentTo(

$user,

function (OrderShipped $notification, $channels) use ($order) {

return $notification->order->id === $order->id;

}

);

Answer: assertSentTo 可帶入 closure, 若有符合 closure 內條件的 mailable sent, 則 assertion 成立

# On-Demand Notifications

以下的 Laravel testing example code 的意思是？

Example:

<?php

use Illuminate\Notifications\AnonymousNotifiable;



Notification::assertSentTo(

new AnonymousNotifiable, OrderShipped::class

);

Answer: 當測試 On-Demand notification 時, 原本帶入 user 的 arg1, 改帶入 AnonymousNotifiable instance

以下的 Laravel testing example code 的意思是？

Example:

<?php

Notification::assertSentTo(

new AnonymousNotifiable,

OrderShipped::class,

function ($notification, $channels, $notifiable) use ($user) {

return $notifiable->routes['mail'] === $user->email;

}

);

Answer: 當測試 on-demand notification 時, 可帶入 closure, assert 實際上發送的 mail 與指定 user 的 mail 相同

# Queue Fake

以下的 Laravel testing example code 的意思是？

Example:

<?php

public function test_orders_can_be_shipped()

{

Queue::fake();



// Perform order shipping...



Queue::assertNothingPushed();



Queue::assertPushedOn('queue-name', ShipOrder::class);



Queue::assertPushed(ShipOrder::class, 2);



Queue::assertNotPushed(AnotherJob::class);

}

Answer: fake queue facade, 所以不會真的把 job push 到 queue 當中 assert 沒有任何 job 被 push assert ShipOrder job 被 push 到 queue-name assert ShipOrder job 被 push 兩次 assert AnotherJob 沒有被 push

以下的 Laravel testing example code 的意思是？

Example:

<?php

Queue::assertPushed(function (ShipOrder $job) use ($order) {

return $job->order->id === $order->id;

});

Answer: 可 pass closure 到 assertPushed(), assertNotPushed(), 更明確的斷言是哪些 job 被 push 或沒被 push

# Job Chains

以下的 Laravel testing example code 的意思是？

Example:

<?php

Queue::assertPushedWithChain(ShipOrder::class, [

RecordShipment::class,

UpdateInventory::class

]);

Answer: 斷言哪些 queue 被 queue chained, arg1 為第一個 job, arg2 為其餘的 jobs

以下的 Laravel testing example code 的意思是？

Example:

<?php

Queue::assertPushedWithChain(ShipOrder::class, [

new RecordShipment,

new UpdateInventory,

]);

Answer: assertPushedWithChain() 可 pass class name, 也可 pass instance

以下的 Laravel testing example code 的意思是？

Example:

<?php

Queue::assertPushedWithoutChain(ShipOrder::class);

Answer: 使用 assertPushedWithoutChain() 來斷言 ShipOrder job 沒有被 chained

# Storage Fake

以下的 Laravel testing example code 的意思是？

Example:

<?php

public function test_albums_can_be_uploaded()

{

Storage::fake('photos');



$response = $this->json('POST', '/photos', [

UploadedFile::fake()->image('photo1.jpg'),

UploadedFile::fake()->image('photo2.jpg')

]);



Storage::disk('photos')->assertExists('photo1.jpg');

Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);



Storage::disk('photos')->assertMissing('missing.jpg');

Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);

}

Answer: 使用 Storage fake() 一個 fake disk 產生 fake image 斷言 fake disk photos 存在某些檔案 斷言 fake disk photos 不存在某些檔案

以下的 Laravel testing example code 的意思是？

Example:

<?php

$response = $this->json('POST', '/photos', [

UploadedFile::fake()->image('photo1.jpg'),

UploadedFile::persistentFake()->image('photo2.jpg')

]);

Answer: 使用 fake 時, 該 fake file 會在 testing 結束後從 temp dir 中被刪除 若要保留, 可使用 persistentFake()

# Interacting With Time

以下的 Laravel testing example code 的意思是？

Example:

<?php

public function testTimeCanBeManipulated()

{

$this->travel(5)->milliseconds();

$this->travel(5)->seconds();

$this->travel(5)->minutes();

$this->travel(5)->hours();

$this->travel(5)->days();

$this->travel(5)->weeks();

$this->travel(5)->years();



$this->travel(-5)->hours();



$this->travelTo(now()->subHours(6));



$this->travelBack();

}

Answer:

<?php

public function testTimeCanBeManipulated()

{

// Travel into the future...

$this->travel(5)->milliseconds();

$this->travel(5)->seconds();

$this->travel(5)->minutes();

$this->travel(5)->hours();

$this->travel(5)->days();

$this->travel(5)->weeks();

$this->travel(5)->years();



// Travel into the past...

$this->travel(-5)->hours();



// Travel to an explicit time...

$this->travelTo(now()->subHours(6));



// Return back to the present time...

$this->travelBack();

}

可使用 travel() method, 定義當前時間以方便測試