測(cè)試模擬器 Mocking
測(cè)試模擬器 Mocking
測(cè)試模擬器
簡(jiǎn)介
在 Laravel 應(yīng)用程序測(cè)試中,你可能希望「模擬」應(yīng)用程序的某些功能的行為,從而避免該部分在測(cè)試中真正執(zhí)行。例如:在控制器執(zhí)行過(guò)程中會(huì)觸發(fā)事件(Event),從而避免該事件在測(cè)試控制器時(shí)真正執(zhí)行。這允許你在僅測(cè)試控制器 HTTP 響應(yīng)的情況時(shí),而不必?fù)?dān)心觸發(fā)事件。當(dāng)然,你也可以在單獨(dú)的測(cè)試中測(cè)試該事件邏輯。
Laravel 針對(duì)事件、任務(wù)和 Facades 的模擬,提供了開(kāi)箱即用的輔助函數(shù)。這些函數(shù)基于 Mocker 封裝而成,使用非常方便,無(wú)需手動(dòng)調(diào)用復(fù)雜的 Mockery 函數(shù)。當(dāng)然你也可以使用 Mockery 或者使用 PHPUnit 創(chuàng)建自己的模擬器。
模擬對(duì)象
當(dāng)模擬一個(gè)對(duì)象將通過(guò) Laravel 的服務(wù)容器注入到應(yīng)用中時(shí),你將需要將模擬實(shí)例作為 instance
綁定到容器中。這將告訴容器使用對(duì)象的模擬實(shí)例,而不是構(gòu)造對(duì)象的真身:
use Mockery; use App\Service; $this->instance(Service::class, Mockery::mock(Service::class, function ($mock) { $mock->shouldReceive('process')->once(); }) );
為了讓以上過(guò)程更加便捷,你可以使用 Laravel 的基本測(cè)試用例類提供 mock
方法:
use App\Service;$this->mock(Service::class, function ($mock) { $mock->shouldReceive('process')->once(); });
任務(wù)模擬
作為模擬的替代方式,你可以使用 Bus
Facade 的 fake
方法來(lái)防止任務(wù)被真正分發(fā)執(zhí)行。使用 fake 的時(shí)候,斷言一般出現(xiàn)在測(cè)試代碼的后面:
<?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(); // 執(zhí)行訂單發(fā)貨... Bus::assertDispatched(ShipOrder::class, function ($job) use ($order) { return $job->order->id === $order->id; }); // 斷言任務(wù)并未分發(fā)... Bus::assertNotDispatched(AnotherJob::class); } }
事件模擬
作為 mock 的替代方法,你可以使用 Event
Facade 的 fake
方法來(lái)模擬事件監(jiān)聽(tīng),測(cè)試的時(shí)候并不會(huì)真正觸發(fā)事件監(jiān)聽(tīng)器。然后你就可以測(cè)試斷言事件運(yùn)行了,甚至可以檢查他們接收的數(shù)據(jù)。使用 fake 的時(shí)候,斷言一般出現(xiàn)在測(cè)試代碼的后面:
<?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{ /** * 測(cè)試訂單發(fā)送 */ public function testOrderShipping() { Event::fake(); // 執(zhí)行訂單發(fā)送... Event::assertDispatched(OrderShipped::class, function ($e) use ($order) { return $e->order->id === $order->id; }); // 斷言一個(gè)事件被發(fā)送了兩次... Event::assertDispatched(OrderShipped::class, 2); // 未分配斷言事件... Event::assertNotDispatched(OrderFailedToShip::class); } }
{note} 調(diào)用
Event::fake()
后不會(huì)執(zhí)行事件監(jiān)聽(tīng)。所以,你基于事件的測(cè)試必須使用工廠模型,例如,在模型的creating
事件中創(chuàng)建 UUID ,你應(yīng)該調(diào)用Event::fake()
之后 使用工廠模型。
模擬事件的子集
如果你只想為特定的一組事件模擬事件監(jiān)聽(tīng)器,你可以將它們傳遞給 fake
或 fakeFor
方法:
/** * 測(cè)試訂單流程 */ public function testOrderProcess(){ Event::fake([ OrderCreated::class, ]); $order = factory(Order::class)->create(); Event::assertDispatched(OrderCreated::class); // 其他事件照常發(fā)送... $order->update([...]); }
Scoped 事件模擬
如果你只想為部分測(cè)試模擬事件監(jiān)聽(tīng),則可以使用 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{ /** * 測(cè)試訂單流程 */ public function testOrderProcess() { $order = Event::fakeFor(function () { $order = factory(Order::class)->create(); Event::assertDispatched(OrderCreated::class); return $order; }); // 事件按正常方式發(fā)送,觀察者將運(yùn)行... $order->update([...]); } }
郵件模擬
你可以是用 Mail
Facade 的 fake
方法來(lái)模擬郵件發(fā)送,測(cè)試時(shí)不會(huì)真的發(fā)送郵件,然后你可以斷言 mailables 發(fā)送給了用戶,甚至可以檢查他們收到的內(nèi)容。使用 fakes 時(shí),斷言一般放在測(cè)試代碼的后面:
<?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(); // 斷言沒(méi)有發(fā)送任何郵件... Mail::assertNothingSent(); // 執(zhí)行訂單發(fā)送... Mail::assertSent(OrderShipped::class, function ($mail) use ($order) { return $mail->order->id === $order->id; }); // 斷言一條發(fā)送給用戶的消息... Mail::assertSent(OrderShipped::class, function ($mail) use ($user) { return $mail->hasTo($user->email) && $mail->hasCc('...') && $mail->hasBcc('...'); }); // 斷言郵件被發(fā)送兩次... Mail::assertSent(OrderShipped::class, 2); // 斷言沒(méi)有發(fā)送郵件... Mail::assertNotSent(AnotherMailable::class); } }
如果你用后臺(tái)任務(wù)執(zhí)行郵件發(fā)送隊(duì)列,你應(yīng)該是用 assertQueued
代替 assertSent
:
Mail::assertQueued(...); Mail::assertNotQueued(...);
通知模擬
你可以使用 Notification
Facade 的 fake
方法來(lái)模擬通知的發(fā)送,測(cè)試時(shí)并不會(huì)真的發(fā)出通知。然后你可以斷言 notifications 發(fā)送給了用戶,甚至可以檢查他們收到的內(nèi)容。使用 fakes 時(shí),斷言一般放在測(cè)試代碼后面:
<?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(); // 斷言沒(méi)有發(fā)送通知... Notification::assertNothingSent(); // 執(zhí)行訂單發(fā)送... Notification::assertSentTo( $user, OrderShipped::class, function ($notification, $channels) use ($order) { return $notification->order->id === $order->id; } ); // 斷言向給定用戶發(fā)送了通知... Notification::assertSentTo( [$user], OrderShipped::class ); // 斷言沒(méi)有發(fā)送通知... Notification::assertNotSentTo( [$user], AnotherNotification::class ); // 斷言通過(guò) Notification::route() 方法發(fā)送通知... Notification::assertSentTo( new AnonymousNotifiable, OrderShipped::class ); } }
隊(duì)列模擬
作為模擬替代方案,你可以使用 Queue
Facade 的 fake
方法避免把任務(wù)真的放到隊(duì)列中執(zhí)行。然后你就可以斷言任務(wù)已經(jīng)被推送入隊(duì)列了,甚至可以檢查它們收到的數(shù)據(jù)。使用 fakes 時(shí),斷言一般放在測(cè)試代碼的后面:
<?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(); // 斷言沒(méi)有任務(wù)被發(fā)送... Queue::assertNothingPushed(); // 執(zhí)行訂單發(fā)送... Queue::assertPushed(ShipOrder::class, function ($job) use ($order) { return $job->order->id === $order->id; }); // 斷言任務(wù)進(jìn)入了指定隊(duì)列... Queue::assertPushedOn('queue-name', ShipOrder::class); // 斷言任務(wù)進(jìn)入2次... Queue::assertPushed(ShipOrder::class, 2); // 斷言沒(méi)有一個(gè)任務(wù)進(jìn)入隊(duì)列... Queue::assertNotPushed(AnotherJob::class); // 斷言任務(wù)是由特定的通道發(fā)送的... Queue::assertPushedWithChain(ShipOrder::class, [ AnotherJob::class, FinalJob::class ]); } }
存儲(chǔ)模擬
你可以使用 Storage
Facade 的 fake
方法,輕松的生成一個(gè)模擬磁盤(pán),結(jié)合 UploadedFile 類的文件生成工具,極大的簡(jiǎn)化了文件上傳測(cè)試。例如:
<?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') ]); // 斷言文件已存儲(chǔ)... Storage::disk('avatars')->assertExists('avatar.jpg'); // 斷言文件不存在... Storage::disk('avatars')->assertMissing('missing.jpg'); } }
{tip} 默認(rèn)情況下,
fake
方法將刪除臨時(shí)目錄下所有文件。如果你想保留這些文件,你可以使用 「persistentFake」。
Facades
與傳統(tǒng)靜態(tài)方法調(diào)用不同的是, facades 也可以被模擬。相較傳統(tǒng)的靜態(tài)方法而言,它具有很大的優(yōu)勢(shì),即便你使用依賴注入,可測(cè)試性不遜半分。在測(cè)試中,你可能想在控制器中模擬對(duì) Laravel Facade 的調(diào)用。比如下面控制器中的行為:
<?php namespace App\Http\Controllers; use Illuminate\Support\Facades\Cache; class UserController extends Controller{ /** * 顯示應(yīng)用里所有用戶 * * @return Response */ public function index() { $value = Cache::get('key'); // } }
我們可以通過(guò) shouldReceive
方法來(lái)模擬 Cache
Facade,此函數(shù)會(huì)返回一個(gè) Mockery 實(shí)例。由于 Facade 的調(diào)用實(shí)際是由 Laravel 的 服務(wù)容器 管理的,所以 Facade 能比傳統(tǒng)的靜態(tài)類表現(xiàn)出更好的可測(cè)試性。下面,讓我們模擬一下 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 。相反,在運(yùn)行測(cè)試時(shí)如果需要傳入指定參數(shù),請(qǐng)使用 HTTP 輔助函數(shù),比如get
和post
。同理,請(qǐng)?jiān)跍y(cè)試時(shí)通過(guò)調(diào)用Config::set
來(lái)模擬Config
Facade。