Test simulator Mocking
- Event Simulation
- Scoped Event Simulation
- Mail simulation
- Notification simulation Storage simulation
- Facades
Introduction
When testing a Laravel application, you may want to "simulate" the behavior of certain features of the application, thereby preventing that part from actually executing in the test. For example: an event (Event) will be triggered during the execution of the controller, thus preventing the event from actually being executed when testing the controller. This allows you to test only the controller's HTTP response without having to worry about triggering events. Of course, you can also test this event logic in a separate test.
Laravel provides out-of-the-box helper functions for the simulation of events, tasks, and Facades. These functions are encapsulated based on Mocker and are very convenient to use. There is no need to manually call complex Mockery functions. Of course, you can also use Mockery or use PHPUnit to create your own emulator.
Mock Object
When mocking an object that will be injected into your application through Laravel's service container, you will The mock instance needs to be bound to the container as
instance
. This will tell the container to use a mock instance of the object instead of constructing the actual object:use Mockery; use App\Service; $this->instance(Service::class, Mockery::mock(Service::class, function ($mock) { $mock->shouldReceive('process')->once(); }) );
To make the above process more convenient, you can use the
mock
method provided by Laravel's base test case class :use App\Service;$this->mock(Service::class, function ($mock) { $mock->shouldReceive('process')->once(); });
Task Simulation
As an alternative to simulation, you can use
Bus
Facade'sfake
method prevents tasks from being actually distributed for execution. When using fake, assertions generally appear behind the test code:<?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); } }
Event simulation
As a mock Alternatively, you can use the
fake
method of theEvent
Facade to simulate the event listener. The event listener will not actually be triggered during testing. You can then test the assertion events running and even inspect the data they receive. When using fake, assertions generally appear behind the test code:<?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 monitoring will not be executed after calling
Event::fake()
. So, your event-based tests must use a factory model, for example, to create a UUID in the model'screating
event, you should callEvent::fake()
after Use factory model.Simulating a subset of events
If you only want to simulate event listeners for a specific set of events, you can pass them to
fake
orfakeFor
Method:/** * 测试订单流程 */ public function testOrderProcess(){ Event::fake([ OrderCreated::class, ]); $order = factory(Order::class)->create(); Event::assertDispatched(OrderCreated::class); // 其他事件照常发送... $order->update([...]); }
Scoped event simulation
If you If you only want to simulate event listening for part of the test, you can use the
fakeFor
method:<?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 Simulation
You can use the
fake
method ofMail
Facade to simulate email sending. The email will not actually be sent during testing, and then you can assert Mailables are sent to users and they can even check what they have received. When using fakes, assertions are generally placed after the test code:<?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); } }
If you use a background task to execute the mail sending queue, you should use
assertQueued
instead ofassertSent
:Mail::assertQueued(...); Mail::assertNotQueued(...);
Notification simulation
You can use
Notification
Facade’sfake
Method to simulate sending notifications. Notifications will not actually be sent during testing. You can then assert that notifications were sent to the user and even inspect what they received. When using fakes, assertions are generally placed after the test code:<?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 simulationAs a simulation alternative, You can use thefake
method of the
QueueFacade to avoid actually putting the task in the queue for execution. You can then assert that tasks have been pushed to the queue and even inspect the data they received. When using fakes, assertions are generally placed after the test code:
<?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 simulationYou can use
StorageThe Facade's
{tip} By default, thefake
method can easily generate a simulated disk. Combined with the file generation tool of the UploadedFile class, it greatly simplifies file upload testing. For example:<?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'); } }
fake
method will delete all files in the temporary directory. If you want to keep these files, you can use "persistentFake".
Facades
Unlike traditional static method calls, facades can also be simulated. Compared with traditional static methods, it has great advantages. Even if you use dependency injection, the testability is not inferior by half. In your tests, you may want to simulate calls to the Laravel Facade in your controller. For example, the behavior in the following controller:
<?php namespace App\Http\Controllers; use Illuminate\Support\Facades\Cache; class UserController extends Controller{ /** * 显示应用里所有用户 * * @return Response */ public function index() { $value = Cache::get('key'); // } }
We can simulate the
Cache
Facade through theshouldReceive
method. This function will return a Mockery instance. . Because Facade calls are actually managed by Laravel's service container, Facade can show better testability than traditional static classes. Next, let's simulate theget
method of theCache
Facade:<?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} You cannot simulate the
Request
Facade. Instead, use HTTP helper functions such asget
andpost
if you need to pass in specific parameters when running your tests. Similarly, please simulate theConfig
Facade by callingConfig::set
during testing.