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;
4 
5public 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;
4 
5public 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;
3 
4$mock = $this->mock(Service::class, function (MockInterface $mock) {
5 $mock->shouldReceive('process')->once();
6});
1use App\Service;
2use Mockery\MockInterface;
3 
4$mock = $this->mock(Service::class, function (MockInterface $mock) {
5 $mock->shouldReceive('process')->once();
6});

若只需要 Mock 某個物件的一部分方法,可使用 partialMock 方法。若呼叫了未被 Mock 的方法,則這些方法會正常執行:

1use App\Service;
2use Mockery\MockInterface;
3 
4$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
5 $mock->shouldReceive('process')->once();
6});
1use App\Service;
2use Mockery\MockInterface;
3 
4$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;
2 
3$spy = $this->spy(Service::class);
4 
5// ...
6 
7$spy->shouldHaveReceived('process');
1use App\Service;
2 
3$spy = $this->spy(Service::class);
4 
5// ...
6 
7$spy->shouldHaveReceived('process');

Mock Facade

與傳統的靜態方法呼叫不同,[Facade] (包含即時 Facade) 是可以被 Mock 的。這樣一來,我們還是能使用傳統的靜態方法呼叫,同時又不會失去傳統相依性插入所帶來的可測試性。在測試時,我們通常會想 Mock 在 Controller 中的某個 Laravel Facade 呼叫。舉例來說,來看看下列 Controller 動作:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Support\Facades\Cache;
6 
7class UserController extends Controller
8{
9 /**
10 * Retrieve a list of all users of the application.
11 *
12 * @return \Illuminate\Http\Response
13 */
14 public function index()
15 {
16 $value = Cache::get('key');
17 
18 //
19 }
20}
1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Support\Facades\Cache;
6 
7class UserController extends Controller
8{
9 /**
10 * Retrieve a list of all users of the application.
11 *
12 * @return \Illuminate\Http\Response
13 */
14 public function index()
15 {
16 $value = Cache::get('key');
17 
18 //
19 }
20}

我們可以使用 shouldReceive 方法來 Mock Cache Facade 的呼叫。該方法會回傳 Mockery 的 Mock 實體。由於Facade 會實際上會由 Laravel 的 Service Container 來解析與管理,因此比起傳統的靜態類別,Facade 有更好的可測試性。舉例來說,我們來 Mock Cache Facade 的 get 方法呼叫:

1<?php
2 
3namespace Tests\Feature;
4 
5use Illuminate\Foundation\Testing\RefreshDatabase;
6use Illuminate\Foundation\Testing\WithoutMiddleware;
7use Illuminate\Support\Facades\Cache;
8use Tests\TestCase;
9 
10class UserControllerTest extends TestCase
11{
12 public function testGetIndex()
13 {
14 Cache::shouldReceive('get')
15 ->once()
16 ->with('key')
17 ->andReturn('value');
18 
19 $response = $this->get('/users');
20 
21 // ...
22 }
23}
1<?php
2 
3namespace Tests\Feature;
4 
5use Illuminate\Foundation\Testing\RefreshDatabase;
6use Illuminate\Foundation\Testing\WithoutMiddleware;
7use Illuminate\Support\Facades\Cache;
8use Tests\TestCase;
9 
10class UserControllerTest extends TestCase
11{
12 public function testGetIndex()
13 {
14 Cache::shouldReceive('get')
15 ->once()
16 ->with('key')
17 ->andReturn('value');
18 
19 $response = $this->get('/users');
20 
21 // ...
22 }
23}
exclamation

請不要 Mock Request Facade。在執行測試時,請將要測試的輸入傳給如 getpost 等的 HTTP 測試方法。類似地,請不要 Mock Config Facade,請在測試中執行 Config::set 方法。

Facade 的 Spy

若想 Spy 某個 Facade,則可在對應的 Facade 上呼叫 spy 方法。Spy 與 Mock 類似;不過,Spy 會記錄所有 Spy 與正在測試的程式碼間的互動,能讓我們在程式碼執行後進行 Assertion:

1use Illuminate\Support\Facades\Cache;
2 
3public function test_values_are_be_stored_in_cache()
4{
5 Cache::spy();
6 
7 $response = $this->get('/');
8 
9 $response->assertStatus(200);
10 
11 Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);
12}
1use Illuminate\Support\Facades\Cache;
2 
3public function test_values_are_be_stored_in_cache()
4{
5 Cache::spy();
6 
7 $response = $this->get('/');
8 
9 $response->assertStatus(200);
10 
11 Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);
12}

Bus Fake

在測試會分派 Job 的程式時,一般來說我們會想判斷給定的 Job 是否有被分派,而不是真的將該 Job 放入 Queue 裡執行。這是因為,Job 的執行一般來說可以在其獨立的測試 Class 中測試。

我們可以使用 Bus Facade 的 fake 方法來避免 Job 被分派到 Queue 中。接著,在測試中執行程式碼後,我們就可以使用 assertDispatchedassertNotDispatched 方法來檢查程式嘗試分派了什麼 Job:

1<?php
2 
3namespace Tests\Feature;
4 
5use App\Jobs\ShipOrder;
6use Illuminate\Foundation\Testing\RefreshDatabase;
7use Illuminate\Foundation\Testing\WithoutMiddleware;
8use Illuminate\Support\Facades\Bus;
9use Tests\TestCase;
10 
11class ExampleTest extends TestCase
12{
13 public function test_orders_can_be_shipped()
14 {
15 Bus::fake();
16 
17 // 進行訂單出貨...
18 
19 // 判斷 Job 已被分派...
20 Bus::assertDispatched(ShipOrder::class);
21 
22 // 判斷 Job 未被分派...
23 Bus::assertNotDispatched(AnotherJob::class);
24 
25 // 判斷 Job 被同步分派...
26 Bus::assertDispatchedSync(AnotherJob::class);
27 
28 // 判斷 Job 未被同步分派...
29 Bus::assertNotDispatchedSync(AnotherJob::class);
30 
31 // 判斷 Job 在 Response 被送出後才分派...
32 Bus::assertDispatchedAfterResponse(AnotherJob::class);
33 
34 // 判斷 Job 並未在 Response 被送出後才分派...
35 Bus::assertNotDispatchedAfterResponse(AnotherJob::class);
36 
37 // 判斷未分派任何 Job...
38 Bus::assertNothingDispatched();
39 }
40}
1<?php
2 
3namespace Tests\Feature;
4 
5use App\Jobs\ShipOrder;
6use Illuminate\Foundation\Testing\RefreshDatabase;
7use Illuminate\Foundation\Testing\WithoutMiddleware;
8use Illuminate\Support\Facades\Bus;
9use Tests\TestCase;
10 
11class ExampleTest extends TestCase
12{
13 public function test_orders_can_be_shipped()
14 {
15 Bus::fake();
16 
17 // 進行訂單出貨...
18 
19 // 判斷 Job 已被分派...
20 Bus::assertDispatched(ShipOrder::class);
21 
22 // 判斷 Job 未被分派...
23 Bus::assertNotDispatched(AnotherJob::class);
24 
25 // 判斷 Job 被同步分派...
26 Bus::assertDispatchedSync(AnotherJob::class);
27 
28 // 判斷 Job 未被同步分派...
29 Bus::assertNotDispatchedSync(AnotherJob::class);
30 
31 // 判斷 Job 在 Response 被送出後才分派...
32 Bus::assertDispatchedAfterResponse(AnotherJob::class);
33 
34 // 判斷 Job 並未在 Response 被送出後才分派...
35 Bus::assertNotDispatchedAfterResponse(AnotherJob::class);
36 
37 // 判斷未分派任何 Job...
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 ]);
9 
10 // ...
11}
1/**
2 * Test order process.
3 */
4public function test_orders_can_be_shipped()
5{
6 Bus::fake([
7 ShipOrder::class,
8 ]);
9 
10 // ...
11}

也可以使用 fakeExcept 方法來 Fake 除了一組特定 Job 外的所有 Event:

1Bus::fake()->except([
2 ShipOrder::class,
3]);
1Bus::fake()->except([
2 ShipOrder::class,
3]);

Job Chain

Bus Facade 的 assertChained 方法可用來判斷是否有分派某個串聯的 JobassertChained 方法接受一組串聯 Job 的陣列作為其第一個引數:

1use App\Jobs\RecordShipment;
2use App\Jobs\ShipOrder;
3use App\Jobs\UpdateInventory;
4use Illuminate\Support\Facades\Bus;
5 
6Bus::assertChained([
7 ShipOrder::class,
8 RecordShipment::class,
9 UpdateInventory::class
10]);
1use App\Jobs\RecordShipment;
2use App\Jobs\ShipOrder;
3use App\Jobs\UpdateInventory;
4use Illuminate\Support\Facades\Bus;
5 
6Bus::assertChained([
7 ShipOrder::class,
8 RecordShipment::class,
9 UpdateInventory::class
10]);

就像上述範例中可看到的一樣,串聯 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;
3 
4Bus::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;
3 
4Bus::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();
2 
3$job->handle();
4 
5$this->assertTrue($batch->cancelled());
6$this->assertEmpty($batch->added);
1[$job, $batch] = (new ShipOrder)->withFakeBatch();
2 
3$job->handle();
4 
5$this->assertTrue($batch->cancelled());
6$this->assertEmpty($batch->added);

Event Fake

在測試會分派 Event 的程式時,我們可能會希望 Laravel 不要真的去執行 Event 的 Listener。使用 Event Facade 的 fake 方法,就可避免執行真正的 Listener,在測試中執行程式碼,然後使用 assertDispatchedassertNotDispatchedassertNothingDispatched 等方法來判斷程式分派了哪些 Event:

1<?php
2 
3namespace Tests\Feature;
4 
5use 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;
11 
12class ExampleTest extends TestCase
13{
14 /**
15 * Test order shipping.
16 */
17 public function test_orders_can_be_shipped()
18 {
19 Event::fake();
20 
21 // 進行訂單出貨...
22 
23 // 判斷某個 Event 是否有被分派...
24 Event::assertDispatched(OrderShipped::class);
25 
26 // 判斷某個 Event 是否有被分派兩次...
27 Event::assertDispatched(OrderShipped::class, 2);
28 
29 // 判斷某個 Event 是否未被分派...
30 Event::assertNotDispatched(OrderFailedToShip::class);
31 
32 // 判斷是否未有 Event 被分派...
33 Event::assertNothingDispatched();
34 }
35}
1<?php
2 
3namespace Tests\Feature;
4 
5use 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;
11 
12class ExampleTest extends TestCase
13{
14 /**
15 * Test order shipping.
16 */
17 public function test_orders_can_be_shipped()
18 {
19 Event::fake();
20 
21 // 進行訂單出貨...
22 
23 // 判斷某個 Event 是否有被分派...
24 Event::assertDispatched(OrderShipped::class);
25 
26 // 判斷某個 Event 是否有被分派兩次...
27 Event::assertDispatched(OrderShipped::class, 2);
28 
29 // 判斷某個 Event 是否未被分派...
30 Event::assertNotDispatched(OrderFailedToShip::class);
31 
32 // 判斷是否未有 Event 被分派...
33 Event::assertNothingDispatched();
34 }
35}

可以傳入一個閉包給 assertDispatchedassertNotDispatched 方法,來判斷某個 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::class
4);
1Event::assertListening(
2 OrderShipped::class,
3 SendShipmentNotification::class
4);
exclamation

呼叫 Event::fake() 後,就不會執行 Event Listener。因此,若有測試使用的 Model Factory 仰賴於 Event,如在 Model 的 creating Event 上建立 UUID 等,請在使用完 Factory 之後 再呼叫 Event::fake()

Fake 一小部分的 Event

若只想為一部分 Event 來 Fake Event Listener,則可將這些 Event 傳入fakefakeFor 方法:

1/**
2 * Test order process.
3 */
4public function test_orders_can_be_processed()
5{
6 Event::fake([
7 OrderCreated::class,
8 ]);
9 
10 $order = Order::factory()->create();
11 
12 Event::assertDispatched(OrderCreated::class);
13 
14 // 其他 Event 會被正常分派...
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 ]);
9 
10 $order = Order::factory()->create();
11 
12 Event::assertDispatched(OrderCreated::class);
13 
14 // 其他 Event 會被正常分派...
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<?php
2 
3namespace Tests\Feature;
4 
5use 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;
11 
12class ExampleTest extends TestCase
13{
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();
21 
22 Event::assertDispatched(OrderCreated::class);
23 
24 return $order;
25 });
26 
27 // Event 會被正常分派,且會執行 Observer...
28 $order->update([...]);
29 }
30}
1<?php
2 
3namespace Tests\Feature;
4 
5use 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;
11 
12class ExampleTest extends TestCase
13{
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();
21 
22 Event::assertDispatched(OrderCreated::class);
23 
24 return $order;
25 });
26 
27 // Event 會被正常分派,且會執行 Observer...
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<?php
2 
3namespace Tests\Feature;
4 
5use App\Mail\OrderShipped;
6use Illuminate\Foundation\Testing\RefreshDatabase;
7use Illuminate\Foundation\Testing\WithoutMiddleware;
8use Illuminate\Support\Facades\Mail;
9use Tests\TestCase;
10 
11class ExampleTest extends TestCase
12{
13 public function test_orders_can_be_shipped()
14 {
15 Mail::fake();
16 
17 // 進行訂單出貨...
18 
19 // 判斷未有 Mailable 被寄出...
20 Mail::assertNothingSent();
21 
22 // 判斷某個 Mailable 有被寄出...
23 Mail::assertSent(OrderShipped::class);
24 
25 // 判斷某個 Mailable 有被寄出兩次...
26 Mail::assertSent(OrderShipped::class, 2);
27 
28 // 判斷某個 Mailable 是否未被寄出...
29 Mail::assertNotSent(AnotherMailable::class);
30 }
31}
1<?php
2 
3namespace Tests\Feature;
4 
5use App\Mail\OrderShipped;
6use Illuminate\Foundation\Testing\RefreshDatabase;
7use Illuminate\Foundation\Testing\WithoutMiddleware;
8use Illuminate\Support\Facades\Mail;
9use Tests\TestCase;
10 
11class ExampleTest extends TestCase
12{
13 public function test_orders_can_be_shipped()
14 {
15 Mail::fake();
16 
17 // 進行訂單出貨...
18 
19 // 判斷未有 Mailable 被寄出...
20 Mail::assertNothingSent();
21 
22 // 判斷某個 Mailable 有被寄出...
23 Mail::assertSent(OrderShipped::class);
24 
25 // 判斷某個 Mailable 有被寄出兩次...
26 Mail::assertSent(OrderShipped::class, 2);
27 
28 // 判斷某個 Mailable 是否未被寄出...
29 Mail::assertNotSent(AnotherMailable::class);
30 }
31}

若將 Mailable 放在佇列中以在背景寄送,請使用 assertQueued 方法,而不是 assertSent 方法:

1Mail::assertQueued(OrderShipped::class);
2 
3Mail::assertNotQueued(OrderShipped::class);
4 
5Mail::assertNothingQueued();
1Mail::assertQueued(OrderShipped::class);
2 
3Mail::assertNotQueued(OrderShipped::class);
4 
5Mail::assertNothingQueued();

可以傳入一個閉包給 assertSentassertNotSentassertQueuedassertNotQueued 方法來判斷 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;
2 
3Mail::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});
10 
11Mail::assertSent(OrderShipped::class, function ($mail) {
12 return $mail->hasAttachment(
13 Attachment::fromStorageDisk('s3', '/path/to/file')
14 );
15});
16 
17Mail::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;
2 
3Mail::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});
10 
11Mail::assertSent(OrderShipped::class, function ($mail) {
12 return $mail->hasAttachment(
13 Attachment::fromStorageDisk('s3', '/path/to/file')
14 );
15});
16 
17Mail::assertSent(OrderShipped::class, function ($mail) use ($pdfData) {
18 return $mail->hasAttachment(
19 Attachment::fromData(fn () => $pdfData, 'name.pdf')
20 );
21});

讀者可能已經注意到,總共有兩個方法可用來檢查郵件是否未被送出:assertNotSentassertNotQueued。有時候,我們可能會希望判斷沒有任何郵件被寄出,而且 也沒有任何郵件被放入佇列。若要判斷是否沒有郵件被寄出或放入佇列,可使用 assertNothingOutgoingassertNotOutgoing 方法:

1Mail::assertNothingOutgoing();
2 
3Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {
4 return $mail->order->id === $order->id;
5});
1Mail::assertNothingOutgoing();
2 
3Mail::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<?php
2 
3namespace Tests\Feature;
4 
5use App\Notifications\OrderShipped;
6use Illuminate\Foundation\Testing\RefreshDatabase;
7use Illuminate\Foundation\Testing\WithoutMiddleware;
8use Illuminate\Support\Facades\Notification;
9use Tests\TestCase;
10 
11class ExampleTest extends TestCase
12{
13 public function test_orders_can_be_shipped()
14 {
15 Notification::fake();
16 
17 // 處理訂單出貨...
18 
19 // 判斷未有 Notification 被送出...
20 Notification::assertNothingSent();
21 
22 // 判斷某個 Notification 是否被傳送至給定的使用者...
23 Notification::assertSentTo(
24 [$user], OrderShipped::class
25 );
26 
27 // 判斷某個 Notification 是否未被送出...
28 Notification::assertNotSentTo(
29 [$user], AnotherNotification::class
30 );
31 
32 // 測試送出了給定數量的 Notification...
33 Notification::assertCount(3);
34 }
35}
1<?php
2 
3namespace Tests\Feature;
4 
5use App\Notifications\OrderShipped;
6use Illuminate\Foundation\Testing\RefreshDatabase;
7use Illuminate\Foundation\Testing\WithoutMiddleware;
8use Illuminate\Support\Facades\Notification;
9use Tests\TestCase;
10 
11class ExampleTest extends TestCase
12{
13 public function test_orders_can_be_shipped()
14 {
15 Notification::fake();
16 
17 // 處理訂單出貨...
18 
19 // 判斷未有 Notification 被送出...
20 Notification::assertNothingSent();
21 
22 // 判斷某個 Notification 是否被傳送至給定的使用者...
23 Notification::assertSentTo(
24 [$user], OrderShipped::class
25 );
26 
27 // 判斷某個 Notification 是否未被送出...
28 Notification::assertNotSentTo(
29 [$user], AnotherNotification::class
30 );
31 
32 // 測試送出了給定數量的 Notification...
33 Notification::assertCount(3);
34 }
35}

可以傳入一個閉包給 assertSentToassertNotSentTo 方法,來判斷某個 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<?php
2 
3namespace Tests\Feature;
4 
5use 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;
12 
13class ExampleTest extends TestCase
14{
15 public function test_orders_can_be_shipped()
16 {
17 Queue::fake();
18 
19 // 進行訂單出貨...
20 
21 // 判斷是否未有 Job 被推入...
22 Queue::assertNothingPushed();
23 
24 // 判斷某個 Job 是否被推入到給定的佇列中...
25 Queue::assertPushedOn('queue-name', ShipOrder::class);
26 
27 // 判斷某個 Job 是否被推入到佇列中兩次...
28 Queue::assertPushed(ShipOrder::class, 2);
29 
30 // 判斷某個 Job 是否未被推入佇列...
31 Queue::assertNotPushed(AnotherJob::class);
32 }
33}
1<?php
2 
3namespace Tests\Feature;
4 
5use 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;
12 
13class ExampleTest extends TestCase
14{
15 public function test_orders_can_be_shipped()
16 {
17 Queue::fake();
18 
19 // 進行訂單出貨...
20 
21 // 判斷是否未有 Job 被推入...
22 Queue::assertNothingPushed();
23 
24 // 判斷某個 Job 是否被推入到給定的佇列中...
25 Queue::assertPushedOn('queue-name', ShipOrder::class);
26 
27 // 判斷某個 Job 是否被推入到佇列中兩次...
28 Queue::assertPushed(ShipOrder::class, 2);
29 
30 // 判斷某個 Job 是否未被推入佇列...
31 Queue::assertNotPushed(AnotherJob::class);
32 }
33}

可以傳入一個閉包給 assertPushedassertNotPushed 方法,來判斷某個 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 ]);
6 
7 // 進行訂單出貨...
8 
9 // 判斷 Job 是否被推入 2 次...
10 Queue::assertPushed(ShipOrder::class, 2);
11}
1public function test_orders_can_be_shipped()
2{
3 Queue::fake([
4 ShipOrder::class,
5 ]);
6 
7 // 進行訂單出貨...
8 
9 // 判斷 Job 是否被推入 2 次...
10 Queue::assertPushed(ShipOrder::class, 2);
11}

Job Chain

Queue Facade 的 assertPushedWithChainassertPushedWithoutChain 方法可用來檢查串聯 Job 或是被推入佇列的 Job。assertPushedWithChain 方法的第一個引數未主要的 Job,而第二個引數則為一組包含串聯 Job 的陣列:

1use App\Jobs\RecordShipment;
2use App\Jobs\ShipOrder;
3use App\Jobs\UpdateInventory;
4use Illuminate\Support\Facades\Queue;
5 
6Queue::assertPushedWithChain(ShipOrder::class, [
7 RecordShipment::class,
8 UpdateInventory::class
9]);
1use App\Jobs\RecordShipment;
2use App\Jobs\ShipOrder;
3use App\Jobs\UpdateInventory;
4use Illuminate\Support\Facades\Queue;
5 
6Queue::assertPushedWithChain(ShipOrder::class, [
7 RecordShipment::class,
8 UpdateInventory::class
9]);

就像上述範例中可看到的一樣,串聯 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<?php
2 
3namespace Tests\Feature;
4 
5use Illuminate\Foundation\Testing\RefreshDatabase;
6use Illuminate\Foundation\Testing\WithoutMiddleware;
7use Illuminate\Http\UploadedFile;
8use Illuminate\Support\Facades\Storage;
9use Tests\TestCase;
10 
11class ExampleTest extends TestCase
12{
13 public function test_albums_can_be_uploaded()
14 {
15 Storage::fake('photos');
16 
17 $response = $this->json('POST', '/photos', [
18 UploadedFile::fake()->image('photo1.jpg'),
19 UploadedFile::fake()->image('photo2.jpg')
20 ]);
21 
22 // 判斷保存了一個或多個檔案...
23 Storage::disk('photos')->assertExists('photo1.jpg');
24 Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);
25 
26 // 判斷未保存一個或多個檔案...
27 Storage::disk('photos')->assertMissing('missing.jpg');
28 Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);
29 
30 // 判斷給定目錄是否為空...
31 Storage::disk('photos')->assertDirectoryEmpty('/wallpapers');
32 }
33}
1<?php
2 
3namespace Tests\Feature;
4 
5use Illuminate\Foundation\Testing\RefreshDatabase;
6use Illuminate\Foundation\Testing\WithoutMiddleware;
7use Illuminate\Http\UploadedFile;
8use Illuminate\Support\Facades\Storage;
9use Tests\TestCase;
10 
11class ExampleTest extends TestCase
12{
13 public function test_albums_can_be_uploaded()
14 {
15 Storage::fake('photos');
16 
17 $response = $this->json('POST', '/photos', [
18 UploadedFile::fake()->image('photo1.jpg'),
19 UploadedFile::fake()->image('photo2.jpg')
20 ]);
21 
22 // 判斷保存了一個或多個檔案...
23 Storage::disk('photos')->assertExists('photo1.jpg');
24 Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);
25 
26 // 判斷未保存一個或多個檔案...
27 Storage::disk('photos')->assertMissing('missing.jpg');
28 Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);
29 
30 // 判斷給定目錄是否為空...
31 Storage::disk('photos')->assertDirectoryEmpty('/wallpapers');
32 }
33}

預設情況下,fake 方法會刪除其臨時目錄下的所有檔案。若想保留這些檔案,可使用「persistentFake」方法。更多有關測試檔案上傳的資訊,可參考 HTTP 測試說明文件中有關檔案上傳的部分

exclamation

要使用 image 方法則需要有 GD 擴充程式

處理時間

在測試的時候,我們有時候會想要更改如 nowIlluminate\Support\Carbon::now() 等輔助函式所回傳的時間。幸好,Laravel 的基礎功能測試 (Feature Test) Class 中,有包含一個可以更改目前時間的輔助函式:

1use Illuminate\Support\Carbon;
2 
3public function testTimeCanBeManipulated()
4{
5 // 穿越到未來...
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();
13 
14 // 停止時間,並在執行完閉包後恢復回正常的時間...
15 $this->freezeTime(function (Carbon $time) {
16 // ...
17 });
18 
19 // 穿越到過去...
20 $this->travel(-5)->hours();
21 
22 // 穿越到特定的時間...
23 $this->travelTo(now()->subHours(6));
24 
25 // 回到目前時間...
26 $this->travelBack();
27}
1use Illuminate\Support\Carbon;
2 
3public function testTimeCanBeManipulated()
4{
5 // 穿越到未來...
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();
13 
14 // 停止時間,並在執行完閉包後恢復回正常的時間...
15 $this->freezeTime(function (Carbon $time) {
16 // ...
17 });
18 
19 // 穿越到過去...
20 $this->travel(-5)->hours();
21 
22 // 穿越到特定的時間...
23 $this->travelTo(now()->subHours(6));
24 
25 // 回到目前時間...
26 $this->travelBack();
27}
翻譯進度
100% 已翻譯
更新時間:
2023年2月11日 下午12:59:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.