翻譯進度
53.11% 已翻譯
更新時間:
2024年6月30日 上午8:15:00 [世界標準時間]
翻譯人員:
幫我們翻譯此頁

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 
5test('something can be mocked', function () {
6 $this->instance(
7 Service::class,
8 Mockery::mock(Service::class, function (MockInterface $mock) {
9 $mock->shouldReceive('process')->once();
10 })
11 );
12});
1use App\Service;
2use Mockery;
3use Mockery\MockInterface;
4 
5test('something can be mocked', function () {
6 $this->instance(
7 Service::class,
8 Mockery::mock(Service::class, function (MockInterface $mock) {
9 $mock->shouldReceive('process')->once();
10 })
11 );
12});
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 
3use Illuminate\Support\Facades\Cache;
4 
5test('get index', function () {
6 Cache::shouldReceive('get')
7 ->once()
8 ->with('key')
9 ->andReturn('value');
10 
11 $response = $this->get('/users');
12 
13 // ...
14});
1<?php
2 
3use Illuminate\Support\Facades\Cache;
4 
5test('get index', function () {
6 Cache::shouldReceive('get')
7 ->once()
8 ->with('key')
9 ->andReturn('value');
10 
11 $response = $this->get('/users');
12 
13 // ...
14});
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}
lightbulb

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

Facade 的 Spy

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

1<?php
2 
3use Illuminate\Support\Facades\Cache;
4 
5test('values are be stored in cache', function () {
6 Cache::spy();
7 
8 $response = $this->get('/');
9 
10 $response->assertStatus(200);
11 
12 Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);
13});
1<?php
2 
3use Illuminate\Support\Facades\Cache;
4 
5test('values are be stored in cache', function () {
6 Cache::spy();
7 
8 $response = $this->get('/');
9 
10 $response->assertStatus(200);
11 
12 Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);
13});
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 中,有包含一個可以更改目前時間的輔助函式:

1test('time can be manipulated', function () {
2 // Travel into the future...
3 $this->travel(5)->milliseconds();
4 $this->travel(5)->seconds();
5 $this->travel(5)->minutes();
6 $this->travel(5)->hours();
7 $this->travel(5)->days();
8 $this->travel(5)->weeks();
9 $this->travel(5)->years();
10 
11 // Travel into the past...
12 $this->travel(-5)->hours();
13 
14 // Travel to an explicit time...
15 $this->travelTo(now()->subHours(6));
16 
17 // Return back to the present time...
18 $this->travelBack();
19});
1test('time can be manipulated', function () {
2 // Travel into the future...
3 $this->travel(5)->milliseconds();
4 $this->travel(5)->seconds();
5 $this->travel(5)->minutes();
6 $this->travel(5)->hours();
7 $this->travel(5)->days();
8 $this->travel(5)->weeks();
9 $this->travel(5)->years();
10 
11 // Travel into the past...
12 $this->travel(-5)->hours();
13 
14 // Travel to an explicit time...
15 $this->travelTo(now()->subHours(6));
16 
17 // Return back to the present time...
18 $this->travelBack();
19});
1public function test_time_can_be_manipulated(): void
2{
3 // Travel into the future...
4 $this->travel(5)->milliseconds();
5 $this->travel(5)->seconds();
6 $this->travel(5)->minutes();
7 $this->travel(5)->hours();
8 $this->travel(5)->days();
9 $this->travel(5)->weeks();
10 $this->travel(5)->years();
11 
12 // Travel into the past...
13 $this->travel(-5)->hours();
14 
15 // Travel to an explicit time...
16 $this->travelTo(now()->subHours(6));
17 
18 // Return back to the present time...
19 $this->travelBack();
20}
1public function test_time_can_be_manipulated(): void
2{
3 // Travel into the future...
4 $this->travel(5)->milliseconds();
5 $this->travel(5)->seconds();
6 $this->travel(5)->minutes();
7 $this->travel(5)->hours();
8 $this->travel(5)->days();
9 $this->travel(5)->weeks();
10 $this->travel(5)->years();
11 
12 // Travel into the past...
13 $this->travel(-5)->hours();
14 
15 // Travel to an explicit time...
16 $this->travelTo(now()->subHours(6));
17 
18 // Return back to the present time...
19 $this->travelBack();
20}

也可以提供一個閉包給各個時間旅行方法。呼叫該閉包時,會傳入所凍結的特定時間。執行該閉包後,時間就會恢復正常:

1$this->travel(5)->days(function () {
2 // Test something five days into the future...
3});
4 
5$this->travelTo(now()->subDays(10), function () {
6 // Test something during a given moment...
7});
1$this->travel(5)->days(function () {
2 // Test something five days into the future...
3});
4 
5$this->travelTo(now()->subDays(10), function () {
6 // Test something during a given moment...
7});

freezeTime 方法可用來凍結目前的時間。類似地,freezeSecond 方法會凍結目前時間,並回到目前秒數的開端:

1use Illuminate\Support\Carbon;
2 
3// Freeze time and resume normal time after executing closure...
4$this->freezeTime(function (Carbon $time) {
5 // ...
6});
7 
8// Freeze time at the current second and resume normal time after executing closure...
9$this->freezeSecond(function (Carbon $time) {
10 // ...
11})
1use Illuminate\Support\Carbon;
2 
3// Freeze time and resume normal time after executing closure...
4$this->freezeTime(function (Carbon $time) {
5 // ...
6});
7 
8// Freeze time at the current second and resume normal time after executing closure...
9$this->freezeSecond(function (Carbon $time) {
10 // ...
11})

就像預期的一樣,上方所討論的所有方法主要都適合用來測試與時間相關的程式行為,例如在討論區中鎖定非活躍的貼文:

1use App\Models\Thread;
2 
3test('forum threads lock after one week of inactivity', function () {
4 $thread = Thread::factory()->create();
5 
6 $this->travel(1)->week();
7 
8 expect($thread->isLockedByInactivity())->toBeTrue();
9});
1use App\Models\Thread;
2 
3test('forum threads lock after one week of inactivity', function () {
4 $thread = Thread::factory()->create();
5 
6 $this->travel(1)->week();
7 
8 expect($thread->isLockedByInactivity())->toBeTrue();
9});
1use App\Models\Thread;
2 
3public function test_forum_threads_lock_after_one_week_of_inactivity()
4{
5 $thread = Thread::factory()->create();
6 
7 $this->travel(1)->week();
8 
9 $this->assertTrue($thread->isLockedByInactivity());
10}
1use App\Models\Thread;
2 
3public function test_forum_threads_lock_after_one_week_of_inactivity()
4{
5 $thread = Thread::factory()->create();
6 
7 $this->travel(1)->week();
8 
9 $this->assertTrue($thread->isLockedByInactivity());
10}
翻譯進度
53.11% 已翻譯
更新時間:
2024年6月30日 上午8:15:00 [世界標準時間]
翻譯人員:
幫我們翻譯此頁

留言

尚無留言

“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.