模擬物件 (Mocking)
簡介
在測試 Laravel 應用程式時,您可能希望“模擬”應用程式的某些部分,以便它們在給定的測試中不會真正執行。例如,在測試一個分發事件的控制器時,您可能希望模擬事件監聽器,這樣它們就不會在測試期間真正執行。這使您能夠僅測試控制器的 HTTP 響應,而無需擔心事件監聽器的執行,因為事件監聽器可以在它們自己的測試用例中進行測試。
Laravel 開箱即用地提供了用於模擬事件、作業和其他門面的實用方法。這些輔助工具主要為 Mockery 提供了一層便捷包裝,因此您不必手動編寫複雜的 Mockery 方法呼叫。
模擬物件 (Mocking Objects)
當模擬一個將透過 Laravel 服務容器 注入到您應用程式中的物件時,您需要將模擬例項繫結到容器中作為 instance 繫結。這將指示容器使用您模擬的物件例項,而不是自行構建該物件。
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->expects('process');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->expects('process');11 })12 );13}
為了更方便地實現這一點,您可以使用 Laravel 基礎測試類提供的 mock 方法。例如,以下示例與上述示例等效:
1use App\Service;2use Mockery\MockInterface;3 4$mock = $this->mock(Service::class, function (MockInterface $mock) {5 $mock->expects('process');6});
當您只需要模擬物件的少數幾個方法時,可以使用 partialMock 方法。未被模擬的方法在被呼叫時將正常執行。
1use App\Service;2use Mockery\MockInterface;3 4$mock = $this->partialMock(Service::class, function (MockInterface $mock) {5 $mock->expects('process');6});
同樣,如果您想對某個物件進行 監控 (spy),Laravel 的基礎測試類提供了一個 spy 方法,作為對 Mockery::spy 方法的便捷封裝。間諜 (Spies) 與模擬 (Mocks) 類似;不過,間諜會記錄間諜與被測程式碼之間的所有互動,從而允許您在程式碼執行後進行斷言。
1use App\Service;2 3$spy = $this->spy(Service::class);4 5// ...6 7$spy->shouldHaveReceived('process');
模擬門面 (Mocking Facades)
與傳統的靜態方法呼叫不同,門面 (facades)(包括即時門面)是可以被模擬的。與傳統靜態方法相比,這提供了巨大的優勢,併為您提供了與使用傳統依賴注入相同的可測試性。在測試時,您通常需要模擬控制器中呼叫的 Laravel 門面。例如,考慮以下控制器操作:
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(): array13 {14 $value = Cache::get('key');15 16 return [17 // ...18 ];19 }20}
我們可以使用 expects 方法來模擬對 Cache 門面的呼叫,該方法將返回一個 Mockery 模擬例項。由於門面實際上是由 Laravel 服務容器 解析和管理的,因此它們比典型的靜態類具有更好的可測試性。例如,讓我們模擬對 Cache 門面的 get 方法的呼叫:
1<?php 2 3use Illuminate\Support\Facades\Cache; 4 5test('get index', function () { 6 Cache::expects('get') 7 ->with('key') 8 ->andReturn('value'); 9 10 $response = $this->get('/users');11 12 // ...13});
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(): void11 {12 Cache::expects('get')13 ->with('key')14 ->andReturn('value');15 16 $response = $this->get('/users');17 18 // ...19 }20}
您不應該模擬 Request 門面。相反,在執行測試時,請將所需的輸入傳遞給 HTTP 測試方法(例如 get 和 post)。同樣,與其模擬 Config 門面,不如在測試中呼叫 Config::set 方法。
門面間諜 (Facade Spies)
如果您想對門面進行 監控 (spy),可以在相應的門面上呼叫 spy 方法。間諜與模擬類似;不過,間諜會記錄間諜與被測程式碼之間的所有互動,從而允許您在程式碼執行後進行斷言。
1<?php 2 3use Illuminate\Support\Facades\Cache; 4 5test('values are stored in cache', function () { 6 Cache::spy(); 7 8 $response = $this->get('/'); 9 10 $response->assertStatus(200);11 12 Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);13});
1use Illuminate\Support\Facades\Cache; 2 3public function test_values_are_stored_in_cache(): void 4{ 5 Cache::spy(); 6 7 $response = $this->get('/'); 8 9 $response->assertStatus(200);10 11 Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);12}
時間處理 (Interacting With Time)
在測試時,您偶爾需要修改諸如 now 或 Illuminate\Support\Carbon::now() 等輔助函式返回的時間。值得慶幸的是,Laravel 的基礎功能測試類包含了一些輔助工具,允許您操控當前時間。
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()->minus(hours: 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()->minus(hours: 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()->mins(days: 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 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}