測試模擬器 Mocking


#Facades

  • 測試模擬器
  • 簡介
  • #模擬物件
    • 任務模擬
    事件模擬
  • Scoped 事件模擬
  • 郵件模擬
  • 通知模擬
  • 佇列模擬
Storage 模擬

Facades
######################

簡介

在 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 網站上。