Mock
簡介
在測試 Laravel 專案時,我們有時候會需要「Mock」某部分的程式,好讓執行測試時不要真的執行這一部分程式。舉例來說,在測試會分派 Event 的 Controller 時,我們可能會想 Mock 該 Event 的 Listener,讓這些 Event Listener 在測試階段不要真的被執行。這樣一來,我們就可以只測試 Controller 的 HTTP Response,而不需擔心 Event Listener 的執行,因為這些 Event Listener 可以在其自己的測試例中測試。
Laravel 提供了各種開箱即用的實用方法,可用於 Mock Event、Job、與其他 Facade。這些輔助函式主要提供一個 Mockery 之上的方便層,讓我們不需手動進行複雜的 Mockery 方法呼叫。
Mock 物件
若要 Mock 一些會被 Laravel Service Container 插入到程式中的物件,只需要使用 instance
繫結來將 Mock 後的實體繫結到 Container 中。這樣一來,Container 就會使用 Mock 後的物件實體,而不會再重新建立一個物件:
1use App\Service;2use Mockery;3use Mockery\MockInterface;45public function test_something_can_be_mocked()6{7 $this->instance(8 Service::class,9 Mockery::mock(Service::class, function (MockInterface $mock) {10 $mock->shouldReceive('process')->once();11 })12 );13}
1use App\Service;2use Mockery;3use Mockery\MockInterface;45public function test_something_can_be_mocked()6{7 $this->instance(8 Service::class,9 Mockery::mock(Service::class, function (MockInterface $mock) {10 $mock->shouldReceive('process')->once();11 })12 );13}
為了讓這個過程更方便,我們可以使用 Laravel 基礎測試例 Class 中的 mock
方法。舉例來說,下面這個範例與上一個範例是相等的:
1use App\Service;2use Mockery\MockInterface;34$mock = $this->mock(Service::class, function (MockInterface $mock) {5 $mock->shouldReceive('process')->once();6});
1use App\Service;2use Mockery\MockInterface;34$mock = $this->mock(Service::class, function (MockInterface $mock) {5 $mock->shouldReceive('process')->once();6});
若只需要 Mock 某個物件的一部分方法,可使用 partialMock
方法。若呼叫了未被 Mock 的方法,則這些方法會正常執行:
1use App\Service;2use Mockery\MockInterface;34$mock = $this->partialMock(Service::class, function (MockInterface $mock) {5 $mock->shouldReceive('process')->once();6});
1use App\Service;2use Mockery\MockInterface;34$mock = $this->partialMock(Service::class, function (MockInterface $mock) {5 $mock->shouldReceive('process')->once();6});
類似的,若我們想 Spy 某個物件,Laravel 的基礎測試 Class 中也提供了一個 spy
方法來作為 Mockery::spy
方法的方便包裝。Spy 與 Mock 類似;不過,Spy 會記錄所有 Spy 與正在測試的程式碼間的互動,能讓我們在程式碼執行後進行 Assertion:
1use App\Service;23$spy = $this->spy(Service::class);45// ...67$spy->shouldHaveReceived('process');
1use App\Service;23$spy = $this->spy(Service::class);45// ...67$spy->shouldHaveReceived('process');
Mock Facade
與傳統的靜態方法呼叫不同,[Facade] (包含即時 Facade) 是可以被 Mock 的。這樣一來,我們還是能使用傳統的靜態方法呼叫,同時又不會失去傳統相依性插入所帶來的可測試性。在測試時,我們通常會想 Mock 在 Controller 中的某個 Laravel Facade 呼叫。舉例來說,來看看下列 Controller 動作:
1<?php23namespace App\Http\Controllers;45use Illuminate\Support\Facades\Cache;67class UserController extends Controller8{9 /**10 * Retrieve a list of all users of the application.11 *12 * @return \Illuminate\Http\Response13 */14 public function index()15 {16 $value = Cache::get('key');1718 //19 }20}
1<?php23namespace App\Http\Controllers;45use Illuminate\Support\Facades\Cache;67class UserController extends Controller8{9 /**10 * Retrieve a list of all users of the application.11 *12 * @return \Illuminate\Http\Response13 */14 public function index()15 {16 $value = Cache::get('key');1718 //19 }20}
我們可以使用 shouldReceive
方法來 Mock Cache
Facade 的呼叫。該方法會回傳 Mockery 的 Mock 實體。由於Facade 會實際上會由 Laravel 的 Service Container 來解析與管理,因此比起傳統的靜態類別,Facade 有更好的可測試性。舉例來說,我們來 Mock Cache
Facade 的 get
方法呼叫:
1<?php23namespace Tests\Feature;45use Illuminate\Foundation\Testing\RefreshDatabase;6use Illuminate\Foundation\Testing\WithoutMiddleware;7use Illuminate\Support\Facades\Cache;8use Tests\TestCase;910class UserControllerTest extends TestCase11{12 public function testGetIndex()13 {14 Cache::shouldReceive('get')15 ->once()16 ->with('key')17 ->andReturn('value');1819 $response = $this->get('/users');2021 // ...22 }23}
1<?php23namespace Tests\Feature;45use Illuminate\Foundation\Testing\RefreshDatabase;6use Illuminate\Foundation\Testing\WithoutMiddleware;7use Illuminate\Support\Facades\Cache;8use Tests\TestCase;910class UserControllerTest extends TestCase11{12 public function testGetIndex()13 {14 Cache::shouldReceive('get')15 ->once()16 ->with('key')17 ->andReturn('value');1819 $response = $this->get('/users');2021 // ...22 }23}
請不要 Mock Request
Facade。在執行測試時,請將要測試的輸入傳給如 get
或 post
等的 HTTP 測試方法。類似地,請不要 Mock Config
Facade,請在測試中執行 Config::set
方法。
Facade 的 Spy
若想 Spy 某個 Facade,則可在對應的 Facade 上呼叫 spy
方法。Spy 與 Mock 類似;不過,Spy 會記錄所有 Spy 與正在測試的程式碼間的互動,能讓我們在程式碼執行後進行 Assertion:
1use Illuminate\Support\Facades\Cache;23public function test_values_are_be_stored_in_cache()4{5 Cache::spy();67 $response = $this->get('/');89 $response->assertStatus(200);1011 Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);12}
1use Illuminate\Support\Facades\Cache;23public function test_values_are_be_stored_in_cache()4{5 Cache::spy();67 $response = $this->get('/');89 $response->assertStatus(200);1011 Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);12}
Bus Fake
在測試會分派 Job 的程式時,一般來說我們會想判斷給定的 Job 是否有被分派,而不是真的將該 Job 放入 Queue 裡執行。這是因為,Job 的執行一般來說可以在其獨立的測試 Class 中測試。
我們可以使用 Bus
Facade 的 fake
方法來避免 Job 被分派到 Queue 中。接著,在測試中執行程式碼後,我們就可以使用 assertDispatched
與 assertNotDispatched
方法來檢查程式嘗試分派了什麼 Job:
1<?php23namespace Tests\Feature;45use App\Jobs\ShipOrder;6use Illuminate\Foundation\Testing\RefreshDatabase;7use Illuminate\Foundation\Testing\WithoutMiddleware;8use Illuminate\Support\Facades\Bus;9use Tests\TestCase;1011class ExampleTest extends TestCase12{13 public function test_orders_can_be_shipped()14 {15 Bus::fake();1617 // Perform order shipping...1819 // Assert that a job was dispatched...20 Bus::assertDispatched(ShipOrder::class);2122 // Assert a job was not dispatched...23 Bus::assertNotDispatched(AnotherJob::class);2425 // Assert that a job was dispatched synchronously...26 Bus::assertDispatchedSync(AnotherJob::class);2728 // Assert that a job was not dispatched synchronously...29 Bus::assertNotDispatchedSync(AnotherJob::class);3031 // Assert that a job was dispatched after the response was sent...32 Bus::assertDispatchedAfterResponse(AnotherJob::class);3334 // Assert a job was not dispatched after response was sent...35 Bus::assertNotDispatchedAfterResponse(AnotherJob::class);3637 // Assert no jobs were dispatched...38 Bus::assertNothingDispatched();39 }40}
1<?php23namespace Tests\Feature;45use App\Jobs\ShipOrder;6use Illuminate\Foundation\Testing\RefreshDatabase;7use Illuminate\Foundation\Testing\WithoutMiddleware;8use Illuminate\Support\Facades\Bus;9use Tests\TestCase;1011class ExampleTest extends TestCase12{13 public function test_orders_can_be_shipped()14 {15 Bus::fake();1617 // Perform order shipping...1819 // Assert that a job was dispatched...20 Bus::assertDispatched(ShipOrder::class);2122 // Assert a job was not dispatched...23 Bus::assertNotDispatched(AnotherJob::class);2425 // Assert that a job was dispatched synchronously...26 Bus::assertDispatchedSync(AnotherJob::class);2728 // Assert that a job was not dispatched synchronously...29 Bus::assertNotDispatchedSync(AnotherJob::class);3031 // Assert that a job was dispatched after the response was sent...32 Bus::assertDispatchedAfterResponse(AnotherJob::class);3334 // Assert a job was not dispatched after response was sent...35 Bus::assertNotDispatchedAfterResponse(AnotherJob::class);3637 // Assert no jobs were dispatched...38 Bus::assertNothingDispatched();39 }40}
可以傳入一個閉包給這些可用的方法,來判斷某個 Job 是否通過給定的「真值測試 (Truth Test)」。若分派的 Job 中至少有一個 Job 有通過給真值測試,則 Assertion 會被視為成功。舉例來說,我們可以判斷是否有對某個特定訂單分派 Job:
1Bus::assertDispatched(function (ShipOrder $job) use ($order) {2 return $job->order->id === $order->id;3});
1Bus::assertDispatched(function (ShipOrder $job) use ($order) {2 return $job->order->id === $order->id;3});
Fake 一小部分的 Job
若只想避免特定 Job 被分派,可將要 Fake 的 Job 傳給 fake
方法:
1/**2 * Test order process.3 */4public function test_orders_can_be_shipped()5{6 Bus::fake([7 ShipOrder::class,8 ]);910 // ...11}
1/**2 * Test order process.3 */4public function test_orders_can_be_shipped()5{6 Bus::fake([7 ShipOrder::class,8 ]);910 // ...11}
也可以使用 fakeExcept
方法來 Fake 除了一組特定 Job 外的所有 Event:
1Bus::fake()->except([2 ShipOrder::class,3]);
1Bus::fake()->except([2 ShipOrder::class,3]);
Job Chain
Bus
Facade 的 assertChained
方法可用來判斷是否有分派某個串聯的 Job。assertChained
方法接受一組串聯 Job 的陣列作為其第一個引數:
1use App\Jobs\RecordShipment;2use App\Jobs\ShipOrder;3use App\Jobs\UpdateInventory;4use Illuminate\Support\Facades\Bus;56Bus::assertChained([7 ShipOrder::class,8 RecordShipment::class,9 UpdateInventory::class10]);
1use App\Jobs\RecordShipment;2use App\Jobs\ShipOrder;3use App\Jobs\UpdateInventory;4use Illuminate\Support\Facades\Bus;56Bus::assertChained([7 ShipOrder::class,8 RecordShipment::class,9 UpdateInventory::class10]);
就像上述範例中可看到的一樣,串聯 Job 的陣列就是一組包含 Job 類別名稱的陣列。不過,也可以提供一組實際 Job 實體的陣列。當提供的陣列為 Job 實體的陣列時,Laravel 會確保程式所分派的串聯 Job 都具是相同的類別,且擁有相同的屬性值:
1Bus::assertChained([2 new ShipOrder,3 new RecordShipment,4 new UpdateInventory,5]);
1Bus::assertChained([2 new ShipOrder,3 new RecordShipment,4 new UpdateInventory,5]);
批次 Job
Bus
Facade 的 assertBatched
方法可用來判斷是否有分派[Job 批次]。提供給 assertBatched
方法的閉包會收到 Illuminate\Bus\PendingBatch
的實體,該實體可用來檢查批次中的 Job:
1use Illuminate\Bus\PendingBatch;2use Illuminate\Support\Facades\Bus;34Bus::assertBatched(function (PendingBatch $batch) {5 return $batch->name == 'import-csv' &&6 $batch->jobs->count() === 10;7});
1use Illuminate\Bus\PendingBatch;2use Illuminate\Support\Facades\Bus;34Bus::assertBatched(function (PendingBatch $batch) {5 return $batch->name == 'import-csv' &&6 $batch->jobs->count() === 10;7});
測試 Job 或批次行為
除此之外,我們有時候也需要測試個別 Job 與其底層批次的互動。舉例來說,我們可能需要測試某個 Job 是否取消了批次中剩下的流程。這時,我們需要使用 withFakeBatch
方法來將一個 Fake 的 Batch 指派給該 Job。withFakeBatch
方法會回傳一個包含 Job 實體與 Fake Batch 的陣列:
1[$job, $batch] = (new ShipOrder)->withFakeBatch();23$job->handle();45$this->assertTrue($batch->cancelled());6$this->assertEmpty($batch->added);
1[$job, $batch] = (new ShipOrder)->withFakeBatch();23$job->handle();45$this->assertTrue($batch->cancelled());6$this->assertEmpty($batch->added);
Event Fake
在測試會分派 Event 的程式時,我們可能會希望 Laravel 不要真的去執行 Event 的 Listener。使用 Event
Facade 的 fake
方法,就可避免執行真正的 Listener,在測試中執行程式碼,然後使用 assertDispatched
、assertNotDispatched
、assertNothingDispatched
等方法來判斷程式分派了哪些 Event:
1<?php23namespace Tests\Feature;45use App\Events\OrderFailedToShip;6use App\Events\OrderShipped;7use Illuminate\Foundation\Testing\RefreshDatabase;8use Illuminate\Foundation\Testing\WithoutMiddleware;9use Illuminate\Support\Facades\Event;10use Tests\TestCase;1112class ExampleTest extends TestCase13{14 /**15 * Test order shipping.16 */17 public function test_orders_can_be_shipped()18 {19 Event::fake();2021 // Perform order shipping...2223 // Assert that an event was dispatched...24 Event::assertDispatched(OrderShipped::class);2526 // Assert an event was dispatched twice...27 Event::assertDispatched(OrderShipped::class, 2);2829 // Assert an event was not dispatched...30 Event::assertNotDispatched(OrderFailedToShip::class);3132 // Assert that no events were dispatched...33 Event::assertNothingDispatched();34 }35}
1<?php23namespace Tests\Feature;45use App\Events\OrderFailedToShip;6use App\Events\OrderShipped;7use Illuminate\Foundation\Testing\RefreshDatabase;8use Illuminate\Foundation\Testing\WithoutMiddleware;9use Illuminate\Support\Facades\Event;10use Tests\TestCase;1112class ExampleTest extends TestCase13{14 /**15 * Test order shipping.16 */17 public function test_orders_can_be_shipped()18 {19 Event::fake();2021 // Perform order shipping...2223 // Assert that an event was dispatched...24 Event::assertDispatched(OrderShipped::class);2526 // Assert an event was dispatched twice...27 Event::assertDispatched(OrderShipped::class, 2);2829 // Assert an event was not dispatched...30 Event::assertNotDispatched(OrderFailedToShip::class);3132 // Assert that no events were dispatched...33 Event::assertNothingDispatched();34 }35}
可以傳入一個閉包給 assertDispatched
或 assertNotDispatched
方法,來判斷某個 Event 是否通過給定的「真值測試 (Truth Test)」。若分派的 Event 中至少有一個 Event 通過給定的真值測試,則該 Assertion 會被視為成功:
1Event::assertDispatched(function (OrderShipped $event) use ($order) {2 return $event->order->id === $order->id;3});
1Event::assertDispatched(function (OrderShipped $event) use ($order) {2 return $event->order->id === $order->id;3});
若只想判斷某個 Event Listener 是否有在監聽給定的 Event,可使用 assertListening
方法:
1Event::assertListening(2 OrderShipped::class,3 SendShipmentNotification::class4);
1Event::assertListening(2 OrderShipped::class,3 SendShipmentNotification::class4);
呼叫 Event::fake()
後,就不會執行 Event Listener。因此,若有測試使用的 Model Factory 仰賴於 Event,如在 Model 的 creating
Event 上建立 UUID 等,請在使用完 Factory 之後 再呼叫 Event::fake()
。
Fake 一小部分的 Event
若只想為一部分 Event 來 Fake Event Listener,則可將這些 Event 傳入fake
或 fakeFor
方法:
1/**2 * Test order process.3 */4public function test_orders_can_be_processed()5{6 Event::fake([7 OrderCreated::class,8 ]);910 $order = Order::factory()->create();1112 Event::assertDispatched(OrderCreated::class);1314 // Other events are dispatched as normal...15 $order->update([...]);16}
1/**2 * Test order process.3 */4public function test_orders_can_be_processed()5{6 Event::fake([7 OrderCreated::class,8 ]);910 $order = Order::factory()->create();1112 Event::assertDispatched(OrderCreated::class);1314 // Other events are dispatched as normal...15 $order->update([...]);16}
也可以使用 except
方法來 Fake 除了一組特定 Event 外的所有 Event:
1Event::fake()->except([2 OrderCreated::class,3]);
1Event::fake()->except([2 OrderCreated::class,3]);
限定範圍的 Event Fake
若只想未一部分的測試 Fake Event Listener,則可使用 fakeFor
方法:
1<?php23namespace Tests\Feature;45use App\Events\OrderCreated;6use App\Models\Order;7use Illuminate\Foundation\Testing\RefreshDatabase;8use Illuminate\Support\Facades\Event;9use Illuminate\Foundation\Testing\WithoutMiddleware;10use Tests\TestCase;1112class ExampleTest extends TestCase13{14 /**15 * Test order process.16 */17 public function test_orders_can_be_processed()18 {19 $order = Event::fakeFor(function () {20 $order = Order::factory()->create();2122 Event::assertDispatched(OrderCreated::class);2324 return $order;25 });2627 // Events are dispatched as normal and observers will run ...28 $order->update([...]);29 }30}
1<?php23namespace Tests\Feature;45use App\Events\OrderCreated;6use App\Models\Order;7use Illuminate\Foundation\Testing\RefreshDatabase;8use Illuminate\Support\Facades\Event;9use Illuminate\Foundation\Testing\WithoutMiddleware;10use Tests\TestCase;1112class ExampleTest extends TestCase13{14 /**15 * Test order process.16 */17 public function test_orders_can_be_processed()18 {19 $order = Event::fakeFor(function () {20 $order = Order::factory()->create();2122 Event::assertDispatched(OrderCreated::class);2324 return $order;25 });2627 // Events are dispatched as normal and observers will run ...28 $order->update([...]);29 }30}
HTTP Fake
使用 Http
Facade 的 fake
方法,我們就能讓 HTTP 用戶端在建立 Request 時回傳 Stubbed、假的 Response。更多有關模擬外連 HTTP Request 的資訊,請參考 HTTP 用戶端的測試文件。
Mail Fake
可以使用 Mail
Facade 的 fake
方法來避免寄出 Mail。一般來說,寄送 Mail 與實際要測試的程式碼是不相關的。通常,只要判斷 Laravel 是否有接到指示要寄出給定 Mailable 就夠了。
呼叫 Mail
Facade 的 fake
方法後,就可以判斷是否有被要求要將該 Mailable 寄出給使用者,甚至還能判斷 Mailable 收到的資料:
1<?php23namespace Tests\Feature;45use App\Mail\OrderShipped;6use Illuminate\Foundation\Testing\RefreshDatabase;7use Illuminate\Foundation\Testing\WithoutMiddleware;8use Illuminate\Support\Facades\Mail;9use Tests\TestCase;1011class ExampleTest extends TestCase12{13 public function test_orders_can_be_shipped()14 {15 Mail::fake();1617 // Perform order shipping...1819 // Assert that no mailables were sent...20 Mail::assertNothingSent();2122 // Assert that a mailable was sent...23 Mail::assertSent(OrderShipped::class);2425 // Assert a mailable was sent twice...26 Mail::assertSent(OrderShipped::class, 2);2728 // Assert a mailable was not sent...29 Mail::assertNotSent(AnotherMailable::class);30 }31}
1<?php23namespace Tests\Feature;45use App\Mail\OrderShipped;6use Illuminate\Foundation\Testing\RefreshDatabase;7use Illuminate\Foundation\Testing\WithoutMiddleware;8use Illuminate\Support\Facades\Mail;9use Tests\TestCase;1011class ExampleTest extends TestCase12{13 public function test_orders_can_be_shipped()14 {15 Mail::fake();1617 // Perform order shipping...1819 // Assert that no mailables were sent...20 Mail::assertNothingSent();2122 // Assert that a mailable was sent...23 Mail::assertSent(OrderShipped::class);2425 // Assert a mailable was sent twice...26 Mail::assertSent(OrderShipped::class, 2);2728 // Assert a mailable was not sent...29 Mail::assertNotSent(AnotherMailable::class);30 }31}
若將 Mailable 放在佇列中以在背景寄送,請使用 assertQueued
方法,而不是 assertSent
方法:
1Mail::assertQueued(OrderShipped::class);23Mail::assertNotQueued(OrderShipped::class);45Mail::assertNothingQueued();
1Mail::assertQueued(OrderShipped::class);23Mail::assertNotQueued(OrderShipped::class);45Mail::assertNothingQueued();
可以傳入一個閉包給 assertSent
、assertNotSent
、assertQueued
、assertNotQueued
方法來判斷 Mailable 是否通過給定的「真值測試 (Truth Test)」。若至少有一個寄出的 Mailable 通過給定的真值測試,則該 Assertion 會被視為成功:
1Mail::assertSent(function (OrderShipped $mail) use ($order) {2 return $mail->order->id === $order->id;3});
1Mail::assertSent(function (OrderShipped $mail) use ($order) {2 return $mail->order->id === $order->id;3});
呼叫 Mail
Facade 的 Assertion 方法時,所提供的閉包內收到的 Mailable 實體上有一些實用的方法,可用來檢查 Mailable:
1Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {2 return $mail->hasTo($user->email) &&3 $mail->hasCc('...') &&4 $mail->hasBcc('...') &&5 $mail->hasReplyTo('...') &&6 $mail->hasFrom('...') &&7 $mail->hasSubject('...');8});
1Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {2 return $mail->hasTo($user->email) &&3 $mail->hasCc('...') &&4 $mail->hasBcc('...') &&5 $mail->hasReplyTo('...') &&6 $mail->hasFrom('...') &&7 $mail->hasSubject('...');8});
Mailable 實體也包含了多個實用方法,可用來檢查 Mailable 上的附件:
1use Illuminate\Mail\Mailables\Attachment;23Mail::assertSent(OrderShipped::class, function ($mail) {4 return $mail->hasAttachment(5 Attachment::fromPath('/path/to/file')6 ->as('name.pdf')7 ->withMime('application/pdf')8 );9});1011Mail::assertSent(OrderShipped::class, function ($mail) {12 return $mail->hasAttachment(13 Attachment::fromStorageDisk('s3', '/path/to/file')14 );15});1617Mail::assertSent(OrderShipped::class, function ($mail) use ($pdfData) {18 return $mail->hasAttachment(19 Attachment::fromData(fn () => $pdfData, 'name.pdf')20 );21});
1use Illuminate\Mail\Mailables\Attachment;23Mail::assertSent(OrderShipped::class, function ($mail) {4 return $mail->hasAttachment(5 Attachment::fromPath('/path/to/file')6 ->as('name.pdf')7 ->withMime('application/pdf')8 );9});1011Mail::assertSent(OrderShipped::class, function ($mail) {12 return $mail->hasAttachment(13 Attachment::fromStorageDisk('s3', '/path/to/file')14 );15});1617Mail::assertSent(OrderShipped::class, function ($mail) use ($pdfData) {18 return $mail->hasAttachment(19 Attachment::fromData(fn () => $pdfData, 'name.pdf')20 );21});
讀者可能已經注意到,總共有兩個方法可用來檢查郵件是否未被送出:assertNotSent
、assertNotQueued
。有時候,我們可能會希望判斷沒有任何郵件被寄出,而且 也沒有任何郵件被放入佇列。若要判斷是否沒有郵件被寄出或放入佇列,可使用 assertNothingOutgoing
與 assertNotOutgoing
方法:
1Mail::assertNothingOutgoing();23Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {4 return $mail->order->id === $order->id;5});
1Mail::assertNothingOutgoing();23Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {4 return $mail->order->id === $order->id;5});
測試 Mailable 的內容
在測試郵件是否有寄給特定使用者時,我們建議與 Mailable 的內容分開測試。若要瞭解如何測試郵件是否有寄出,請參考有關測試 Mailable 的說明文件。
Notification Fake
可以使用 Notification
Facade 的 fake
方法來避免送出 Notification。一般來說,送出 Notification 與實際要測試的程式碼是不相關的。通常,只要判斷 Laravel 是否有接到指示要送出給定的 Notification 就夠了。
呼叫 Notification
Facade 的 fake
方法後,就可以判斷是否有被要求要將該 Notification 送出給使用者,甚至還能判斷 Notification 收到的資料:
1<?php23namespace Tests\Feature;45use App\Notifications\OrderShipped;6use Illuminate\Foundation\Testing\RefreshDatabase;7use Illuminate\Foundation\Testing\WithoutMiddleware;8use Illuminate\Support\Facades\Notification;9use Tests\TestCase;1011class ExampleTest extends TestCase12{13 public function test_orders_can_be_shipped()14 {15 Notification::fake();1617 // Perform order shipping...1819 // Assert that no notifications were sent...20 Notification::assertNothingSent();2122 // Assert a notification was sent to the given users...23 Notification::assertSentTo(24 [$user], OrderShipped::class25 );2627 // Assert a notification was not sent...28 Notification::assertNotSentTo(29 [$user], AnotherNotification::class30 );3132 // Assert that a given number of notifications were sent...33 Notification::assertCount(3);34 }35}
1<?php23namespace Tests\Feature;45use App\Notifications\OrderShipped;6use Illuminate\Foundation\Testing\RefreshDatabase;7use Illuminate\Foundation\Testing\WithoutMiddleware;8use Illuminate\Support\Facades\Notification;9use Tests\TestCase;1011class ExampleTest extends TestCase12{13 public function test_orders_can_be_shipped()14 {15 Notification::fake();1617 // Perform order shipping...1819 // Assert that no notifications were sent...20 Notification::assertNothingSent();2122 // Assert a notification was sent to the given users...23 Notification::assertSentTo(24 [$user], OrderShipped::class25 );2627 // Assert a notification was not sent...28 Notification::assertNotSentTo(29 [$user], AnotherNotification::class30 );3132 // Assert that a given number of notifications were sent...33 Notification::assertCount(3);34 }35}
可以傳入一個閉包給 assertSentTo
或 assertNotSentTo
方法,來判斷某個 Notification 是否通過給定的「真值測試 (Truth Test)」。若送出的 Notification 中至少有一個 Notification 通過給定的真值測試,則該 Assertion 會被視為成功:
1Notification::assertSentTo(2 $user,3 function (OrderShipped $notification, $channels) use ($order) {4 return $notification->order->id === $order->id;5 }6);
1Notification::assertSentTo(2 $user,3 function (OrderShipped $notification, $channels) use ($order) {4 return $notification->order->id === $order->id;5 }6);
隨需通知
若要測試的程式中有傳送[隨需通知],則可使用 assertSentOnDemand
方法來測試是否有送出隨需通知:
1Notification::assertSentOnDemand(OrderShipped::class);
1Notification::assertSentOnDemand(OrderShipped::class);
若在 assertSentOnDemand
方法的第二個引數上傳入閉包,就能判斷隨需通知是否被送給正確的「Route」位址:
1Notification::assertSentOnDemand(2 OrderShipped::class,3 function ($notification, $channels, $notifiable) use ($user) {4 return $notifiable->routes['mail'] === $user->email;5 }6);
1Notification::assertSentOnDemand(2 OrderShipped::class,3 function ($notification, $channels, $notifiable) use ($user) {4 return $notifiable->routes['mail'] === $user->email;5 }6);
Queue Fake
可以使用 Queue
Facade 的 fake
方法來避免放入佇列的 Job 被推入到佇列中。通常來說,這麼做就可以判斷 Laravel 有被指示要將給定的 Job 推入到佇列中了。因為,我們可以在其獨立的測試 Class 中測試放入佇列的 Job。
呼叫 Queue
Facade 的 fake
方法後,就可以判斷程式是否有嘗試將 Job 推入到佇列中:
1<?php23namespace Tests\Feature;45use App\Jobs\AnotherJob;6use App\Jobs\FinalJob;7use App\Jobs\ShipOrder;8use Illuminate\Foundation\Testing\RefreshDatabase;9use Illuminate\Foundation\Testing\WithoutMiddleware;10use Illuminate\Support\Facades\Queue;11use Tests\TestCase;1213class ExampleTest extends TestCase14{15 public function test_orders_can_be_shipped()16 {17 Queue::fake();1819 // Perform order shipping...2021 // Assert that no jobs were pushed...22 Queue::assertNothingPushed();2324 // Assert a job was pushed to a given queue...25 Queue::assertPushedOn('queue-name', ShipOrder::class);2627 // Assert a job was pushed twice...28 Queue::assertPushed(ShipOrder::class, 2);2930 // Assert a job was not pushed...31 Queue::assertNotPushed(AnotherJob::class);32 }33}
1<?php23namespace Tests\Feature;45use App\Jobs\AnotherJob;6use App\Jobs\FinalJob;7use App\Jobs\ShipOrder;8use Illuminate\Foundation\Testing\RefreshDatabase;9use Illuminate\Foundation\Testing\WithoutMiddleware;10use Illuminate\Support\Facades\Queue;11use Tests\TestCase;1213class ExampleTest extends TestCase14{15 public function test_orders_can_be_shipped()16 {17 Queue::fake();1819 // Perform order shipping...2021 // Assert that no jobs were pushed...22 Queue::assertNothingPushed();2324 // Assert a job was pushed to a given queue...25 Queue::assertPushedOn('queue-name', ShipOrder::class);2627 // Assert a job was pushed twice...28 Queue::assertPushed(ShipOrder::class, 2);2930 // Assert a job was not pushed...31 Queue::assertNotPushed(AnotherJob::class);32 }33}
可以傳入一個閉包給 assertPushed
或 assertNotPushed
方法,來判斷某個 Job 是否通過給定的「真值測試 (Truth Test)」。若被推入的 Job 中至少有一個 Job 通過給定的真值測試,則該 Assertion 會被視為成功:
1Queue::assertPushed(function (ShipOrder $job) use ($order) {2 return $job->order->id === $order->id;3});
1Queue::assertPushed(function (ShipOrder $job) use ($order) {2 return $job->order->id === $order->id;3});
若只想 Fake 特定的 Job,並讓其他 Job 都被正常執行,可以傳入要被 Fake 的 Job 類別名稱給 fake
方法:
1public function test_orders_can_be_shipped()2{3 Queue::fake([4 ShipOrder::class,5 ]);67 // Perform order shipping...89 // Assert a job was pushed twice...10 Queue::assertPushed(ShipOrder::class, 2);11}
1public function test_orders_can_be_shipped()2{3 Queue::fake([4 ShipOrder::class,5 ]);67 // Perform order shipping...89 // Assert a job was pushed twice...10 Queue::assertPushed(ShipOrder::class, 2);11}
Job Chain
Queue
Facade 的 assertPushedWithChain
與 assertPushedWithoutChain
方法可用來檢查串聯 Job 或是被推入佇列的 Job。assertPushedWithChain
方法的第一個引數未主要的 Job,而第二個引數則為一組包含串聯 Job 的陣列:
1use App\Jobs\RecordShipment;2use App\Jobs\ShipOrder;3use App\Jobs\UpdateInventory;4use Illuminate\Support\Facades\Queue;56Queue::assertPushedWithChain(ShipOrder::class, [7 RecordShipment::class,8 UpdateInventory::class9]);
1use App\Jobs\RecordShipment;2use App\Jobs\ShipOrder;3use App\Jobs\UpdateInventory;4use Illuminate\Support\Facades\Queue;56Queue::assertPushedWithChain(ShipOrder::class, [7 RecordShipment::class,8 UpdateInventory::class9]);
就像上述範例中可看到的一樣,串聯 Job 的陣列就是一組包含 Job 類別名稱的陣列。不過,也可以提供一組實際 Job 實體的陣列。當提供的陣列為 Job 實體的陣列時,Laravel 會確保程式所分派的串聯 Job 都具是相同的類別,且擁有相同的屬性值:
1Queue::assertPushedWithChain(ShipOrder::class, [2 new RecordShipment,3 new UpdateInventory,4]);
1Queue::assertPushedWithChain(ShipOrder::class, [2 new RecordShipment,3 new UpdateInventory,4]);
可以使用 assertPushedWithoutChain
方法來判斷 Job 被推入 Queue 但未包含串聯 Job:
1Queue::assertPushedWithoutChain(ShipOrder::class);
1Queue::assertPushedWithoutChain(ShipOrder::class);
Storage Fake
使用 Storage
Facade 的 fake
方法就可輕鬆地產生 Fake Disk。Fake Disk 可以與 Illuminate\Http\UploadedFile
類別的檔案產生工具來搭配使用,讓我們能非常輕鬆地測試檔案上傳。舉例來說:
1<?php23namespace Tests\Feature;45use Illuminate\Foundation\Testing\RefreshDatabase;6use Illuminate\Foundation\Testing\WithoutMiddleware;7use Illuminate\Http\UploadedFile;8use Illuminate\Support\Facades\Storage;9use Tests\TestCase;1011class ExampleTest extends TestCase12{13 public function test_albums_can_be_uploaded()14 {15 Storage::fake('photos');1617 $response = $this->json('POST', '/photos', [18 UploadedFile::fake()->image('photo1.jpg'),19 UploadedFile::fake()->image('photo2.jpg')20 ]);2122 // Assert one or more files were stored...23 Storage::disk('photos')->assertExists('photo1.jpg');24 Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);2526 // Assert one or more files were not stored...27 Storage::disk('photos')->assertMissing('missing.jpg');28 Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);2930 // Assert that a given directory is empty...31 Storage::disk('photos')->assertDirectoryEmpty('/wallpapers');32 }33}
1<?php23namespace Tests\Feature;45use Illuminate\Foundation\Testing\RefreshDatabase;6use Illuminate\Foundation\Testing\WithoutMiddleware;7use Illuminate\Http\UploadedFile;8use Illuminate\Support\Facades\Storage;9use Tests\TestCase;1011class ExampleTest extends TestCase12{13 public function test_albums_can_be_uploaded()14 {15 Storage::fake('photos');1617 $response = $this->json('POST', '/photos', [18 UploadedFile::fake()->image('photo1.jpg'),19 UploadedFile::fake()->image('photo2.jpg')20 ]);2122 // Assert one or more files were stored...23 Storage::disk('photos')->assertExists('photo1.jpg');24 Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);2526 // Assert one or more files were not stored...27 Storage::disk('photos')->assertMissing('missing.jpg');28 Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);2930 // Assert that a given directory is empty...31 Storage::disk('photos')->assertDirectoryEmpty('/wallpapers');32 }33}
預設情況下,fake
方法會刪除其臨時目錄下的所有檔案。若想保留這些檔案,可使用「persistentFake」方法。更多有關測試檔案上傳的資訊,可參考 HTTP 測試說明文件中有關檔案上傳的部分。
要使用 image
方法則需要有 GD 擴充程式。
處理時間
在測試的時候,我們有時候會想要更改如 now
或 Illuminate\Support\Carbon::now()
等輔助函式所回傳的時間。幸好,Laravel 的基礎功能測試 (Feature Test) Class 中,有包含一個可以更改目前時間的輔助函式:
1use Illuminate\Support\Carbon;23public function testTimeCanBeManipulated()4{5 // Travel into the future...6 $this->travel(5)->milliseconds();7 $this->travel(5)->seconds();8 $this->travel(5)->minutes();9 $this->travel(5)->hours();10 $this->travel(5)->days();11 $this->travel(5)->weeks();12 $this->travel(5)->years();1314 // Freeze time and resume normal time after executing closure...15 $this->freezeTime(function (Carbon $time) {16 // ...17 });1819 // Travel into the past...20 $this->travel(-5)->hours();2122 // Travel to an explicit time...23 $this->travelTo(now()->subHours(6));2425 // Return back to the present time...26 $this->travelBack();27}
1use Illuminate\Support\Carbon;23public function testTimeCanBeManipulated()4{5 // Travel into the future...6 $this->travel(5)->milliseconds();7 $this->travel(5)->seconds();8 $this->travel(5)->minutes();9 $this->travel(5)->hours();10 $this->travel(5)->days();11 $this->travel(5)->weeks();12 $this->travel(5)->years();1314 // Freeze time and resume normal time after executing closure...15 $this->freezeTime(function (Carbon $time) {16 // ...17 });1819 // Travel into the past...20 $this->travel(-5)->hours();2122 // Travel to an explicit time...23 $this->travelTo(now()->subHours(6));2425 // Return back to the present time...26 $this->travelBack();27}