測試模擬器 Mocking
簡介
在 Laravel 應用程式測試中,你可能會想要「模擬」應用程式的某些功能的行為,從而避免該部分在測試中真正執行。例如:在控制器執行過程中會觸發事件(Event),以避免事件在測試控制器時真正執行。這允許你在僅測試控制器 HTTP 回應的情況時,而不必擔心觸發事件。當然,你也可以在單獨的測試中測試該事件邏輯。
Laravel 針對事件、任務和 Facades 的模擬,提供了開箱即用的輔助函數。這些函數基於 Mocker 封裝而成,使用非常方便,無需手動呼叫複雜的 Mockery 函數。當然你也可以使用 Mockery 或使用 PHPUnit 來建立自己的模擬器。
模擬物件
#當模擬一個物件將透過Laravel 的服務容器注入到應用中時,你將需要將模擬實例作為instance
綁定到容器中。這將告訴容器使用物件的模擬實例,而不是建構物件的真身:
use Mockery; use App\Service; $this->instance(Service::class, Mockery::mock(Service::class, function ($mock) { $mock->shouldReceive('process')->once(); }) );
為了讓以上流程更加便捷,你可以使用Laravel 的基本測試案例類別提供mock
方法:
use App\Service;$this->mock(Service::class, function ($mock) { $mock->shouldReceive('process')->once(); });
任務模擬
作為模擬的替代方式,你可以使用Bus
Facade 的fake
方法來防止任務被真正分發執行。使用fake 的時候,斷言一般出現在測試程式碼的後面:
<?php namespace Tests\Feature; use Tests\TestCase;use App\Jobs\ShipOrder; use Illuminate\Support\Facades\Bus; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutMiddleware; class ExampleTest extends TestCase{ public function testOrderShipping() { Bus::fake(); // 执行订单发货... Bus::assertDispatched(ShipOrder::class, function ($job) use ($order) { return $job->order->id === $order->id; }); // 断言任务并未分发... Bus::assertNotDispatched(AnotherJob::class); } }
事件模擬
作為mock 的替代方法,你可以使用Event
Facade 的fake
方法來模擬事件監聽,測試的時候並不會真正觸發事件監聽器。然後你就可以測試斷言事件運行了,甚至可以檢查他們接收的資料。使用 fake 的時候,斷言一般出現在測試程式碼的後面:
<?php namespace Tests\Feature; use Tests\TestCase;use App\Events\OrderShipped; use App\Events\OrderFailedToShip; use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutMiddleware; class ExampleTest extends TestCase{ /** * 测试订单发送 */ public function testOrderShipping() { Event::fake(); // 执行订单发送... Event::assertDispatched(OrderShipped::class, function ($e) use ($order) { return $e->order->id === $order->id; }); // 断言一个事件被发送了两次... Event::assertDispatched(OrderShipped::class, 2); // 未分配断言事件... Event::assertNotDispatched(OrderFailedToShip::class); } }
{note} 呼叫
Event::fake()
後不會執行事件監聽。所以,你基於事件的測試必須使用工廠模型,例如,在模型的creating
事件中建立UUID ,你應該呼叫Event::fake()
之後 使用工廠模型。
模擬事件的子集
如果你只想為特定的一組事件模擬事件監聽器,你可以將它們傳遞給 fake
或fakeFor
方法:
/** * 测试订单流程 */ public function testOrderProcess(){ Event::fake([ OrderCreated::class, ]); $order = factory(Order::class)->create(); Event::assertDispatched(OrderCreated::class); // 其他事件照常发送... $order->update([...]); }
Scoped 事件模擬
#如果你只想為部分測試模擬事件監聽,則可以使用fakeFor
方法:
<?php namespace Tests\Feature; use App\Order;use Tests\TestCase; use App\Events\OrderCreated; use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutMiddleware; class ExampleTest extends TestCase{ /** * 测试订单流程 */ public function testOrderProcess() { $order = Event::fakeFor(function () { $order = factory(Order::class)->create(); Event::assertDispatched(OrderCreated::class); return $order; }); // 事件按正常方式发送,观察者将运行... $order->update([...]); } }
郵件模擬
你可以是用Mail
Facade 的fake
方法來模擬郵件發送,測試時不會真的發送郵件,然後你可以斷言mailables 發送給了用戶,甚至可以檢查他們收到的內容。使用fakes 時,斷言一般放在測試程式碼的後面:
<?php namespace Tests\Feature; use Tests\TestCase;use App\Mail\OrderShipped; use Illuminate\Support\Facades\Mail; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutMiddleware; class ExampleTest extends TestCase{ public function testOrderShipping() { Mail::fake(); // 断言没有发送任何邮件... Mail::assertNothingSent(); // 执行订单发送... Mail::assertSent(OrderShipped::class, function ($mail) use ($order) { return $mail->order->id === $order->id; }); // 断言一条发送给用户的消息... Mail::assertSent(OrderShipped::class, function ($mail) use ($user) { return $mail->hasTo($user->email) && $mail->hasCc('...') && $mail->hasBcc('...'); }); // 断言邮件被发送两次... Mail::assertSent(OrderShipped::class, 2); // 断言没有发送邮件... Mail::assertNotSent(AnotherMailable::class); } }
如果你用後台任務執行郵件發送佇列,你應該是用assertQueued
取代assertSent
:
Mail::assertQueued(...); Mail::assertNotQueued(...);
通知模擬
你可以使用Notification
Facade 的fake
方法來模擬通知的發送,測試時並不會真的發出通知。然後你可以斷言 notifications 發送給了用戶,甚至可以檢查他們收到的內容。使用fakes 時,斷言一般放在測試程式碼後面:
<?php namespace Tests\Feature; use Tests\TestCase; use App\Notifications\OrderShipped; use Illuminate\Support\Facades\Notification; use Illuminate\Notifications\AnonymousNotifiable; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutMiddleware; class ExampleTest extends TestCase{ public function testOrderShipping() { Notification::fake(); // 断言没有发送通知... Notification::assertNothingSent(); // 执行订单发送... Notification::assertSentTo( $user, OrderShipped::class, function ($notification, $channels) use ($order) { return $notification->order->id === $order->id; } ); // 断言向给定用户发送了通知... Notification::assertSentTo( [$user], OrderShipped::class ); // 断言没有发送通知... Notification::assertNotSentTo( [$user], AnotherNotification::class ); // 断言通过 Notification::route() 方法发送通知... Notification::assertSentTo( new AnonymousNotifiable, OrderShipped::class ); } }
#佇列模擬
作為模擬替代方案,你可以使用Queue
Facade 的fake
方法來避免把任務真的放到佇列中執行。然後你就可以斷言任務已經被推送入隊列了,甚至可以檢查它們收到的資料。使用fakes 時,斷言一般放在測試程式碼的後面:
<?php namespace Tests\Feature; use Tests\TestCase;use App\Jobs\ShipOrder; use Illuminate\Support\Facades\Queue; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutMiddleware; class ExampleTest extends TestCase{ public function testOrderShipping() { Queue::fake(); // 断言没有任务被发送... Queue::assertNothingPushed(); // 执行订单发送... Queue::assertPushed(ShipOrder::class, function ($job) use ($order) { return $job->order->id === $order->id; }); // 断言任务进入了指定队列... Queue::assertPushedOn('queue-name', ShipOrder::class); // 断言任务进入2次... Queue::assertPushed(ShipOrder::class, 2); // 断言没有一个任务进入队列... Queue::assertNotPushed(AnotherJob::class); // 断言任务是由特定的通道发送的... Queue::assertPushedWithChain(ShipOrder::class, [ AnotherJob::class, FinalJob::class ]); } }
#儲存模擬
#你可以使用Storage
Facade 的fake
方法,輕鬆的產生一個模擬磁碟,結合UploadedFile 類別的檔案產生工具,極大的簡化了檔案上傳測試。例如:
<?php namespace Tests\Feature; use Tests\TestCase; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutMiddleware; class ExampleTest extends TestCase{ public function testAvatarUpload() { Storage::fake('avatars'); $response = $this->json('POST', '/avatar', [ 'avatar' => UploadedFile::fake()->image('avatar.jpg') ]); // 断言文件已存储... Storage::disk('avatars')->assertExists('avatar.jpg'); // 断言文件不存在... Storage::disk('avatars')->assertMissing('missing.jpg'); } }
#{tip} 預設情況下,
fake
方法將刪除臨時目錄下所有檔案。如果你想保留這些文件,你可以使用 “persistentFake”。
Facades
與傳統靜態方法呼叫不同的是, facades 也可以被模擬。相較傳統的靜態方法而言,它具有很大的優勢,即便你使用依賴注入,可測試性不遜半分。在測試中,你可能想要在控制器中模擬對 Laravel Facade 的呼叫。例如下面控制器中的行為:
<?php namespace App\Http\Controllers; use Illuminate\Support\Facades\Cache; class UserController extends Controller{ /** * 显示应用里所有用户 * * @return Response */ public function index() { $value = Cache::get('key'); // } }
我們可以透過shouldReceive
方法來模擬Cache
Facade,此函數會傳回一個Mockery 實例。由於 Facade 的呼叫實際上是由 Laravel 的 服務容器 管理的,所以 Facade 能比傳統的靜態類別表現出更好的可測試性。下面,讓我們模擬一下 Cache
Facade 的 get
方法:
<?php namespace Tests\Feature; use Tests\TestCase; use Illuminate\Support\Facades\Cache; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithoutMiddleware; class UserControllerTest extends TestCase{ public function testGetIndex() { Cache::shouldReceive('get') ->once() ->with('key') ->andReturn('value'); $response = $this->get('/users'); // ... } }
{note} 你不能模擬
Request
Facade 。相反,在執行測試時如果需要傳入指定參數,請使用 HTTP 輔助函數,例如get
和post
。同理,請在測試時透過呼叫Config::set
來模擬Config
Facade。