测试模拟器 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() 之后 使用工厂模型。

模拟事件的子集

如果你只想为特定的一组事件模拟事件监听器,你可以将它们传递给 fakefakeFor 方法:

/**
 * 测试订单流程
 */
 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 辅助函数,比如 getpost 。同理,请在测试时通过调用 Config::set 来模拟 Config Facade。

本文章首发在 LearnKu.com 网站上。