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(): void
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(): void
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 public function index(): array
13 {
14 $value = Cache::get('key');
15 
16 return [
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 public function index(): array
13 {
14 $value = Cache::get('key');
15 
16 return [
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\Support\Facades\Cache;
6use Tests\TestCase;
7 
8class UserControllerTest extends TestCase
9{
10 public function test_get_index(): void
11 {
12 Cache::shouldReceive('get')
13 ->once()
14 ->with('key')
15 ->andReturn('value');
16 
17 $response = $this->get('/users');
18 
19 // ...
20 }
21}
1<?php
2 
3namespace Tests\Feature;
4 
5use Illuminate\Support\Facades\Cache;
6use Tests\TestCase;
7 
8class UserControllerTest extends TestCase
9{
10 public function test_get_index(): void
11 {
12 Cache::shouldReceive('get')
13 ->once()
14 ->with('key')
15 ->andReturn('value');
16 
17 $response = $this->get('/users');
18 
19 // ...
20 }
21}
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(): void
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(): void
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}

處理時間

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

1use Illuminate\Support\Carbon;
2 
3public function testTimeCanBeManipulated(): void
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(): void
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% 已翻譯
更新時間:
2024年6月30日 上午8:15: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.