laravel 基礎教程 —— 測試

weixin_33749242發表於2016-06-19

測試

簡介

測試是 Laravel 構建的核心理念。事實上,Laravel 開箱即用的支援 PHPUnit 測試,你的應用的根目錄包含了 phpunit.xml 檔案。同時,Laravel 也附帶了一些方便的幫助方法可以使你豐滿應用的測試。

tests 目錄中提供了一個 ExampleTest.php 檔案。在安裝完成 Laravel 應用之後,你只需要在根目錄執行 phpunit 命令就可以執行測試。

測試環境

當執行測試時,Laravel 會自動的設定配置環境為 testing。同時,Laravel 會自動的配置 session 和 快取為 array 驅動。這意味著會話或者快取資料在測試期間不會被持久化。

如果你需要,你完全可以自己建立一個測試環境。testing 環境變數是在 phpunit.xml 檔案中被配置的,但是你要確保在執行測試之前先執行 config:clear Artisan 命令來清除配置快取。

定義 & 執行測試

你可以使用 make:test Artisan 命令來建立一個測試用例:

php artisan make:test UserTest

這個命令會在 tests 目錄下建立一個新的 UserTest 類。你可以像使用 PHPUnit 一樣接著在類中定義一些測試方法。然後你只需要在終端執行 phpunit 命令就可以執行測試:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrationgs;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class UserTest extends TestCase
{
  /**
   * A basic test example.
   *
   * @return void
   */
   public function testExample()
   {
     $this->assetTrue(true);
   }
}

注意:如果你在測試用例中定義了自己的 setUp 方法,你需要確保優先呼叫 parent::setUp 方法。

應用測試

Laravel 提供非常順暢的 API 來構建 HTTP 請求檢查輸出甚至是填充表格。比如,讓我們看下 tests 目錄下的 ExampleTest.php 檔案:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
  /**
   * A basic functional test example
   *
   * @return void
   */
   public function testBasicExample()
   {
     $this->visit('/')
          ->see('Laravel 5')
          ->dontSee('Rails');
   }
}

visit 方法可以在應用中構造一個 GET 請求。see 方法會斷言我們應該從應用的響應中看到所給定的文字。dontSee 方法剛好與 see 相反,它斷言我們不應該看到給定的文字。這就是 Laravel 提供的最基本的應用測試。

與應用進行互動

當然,你可以比這種簡單的斷言響應給定的文字做的更多。讓我們來看一些點選連結和填充表單的例子:

點選超鏈

在這個測試,我們會為應用構造一個請求,在響應中會返回一個可點選的超鏈,並且我們會斷言它應該導向給定的 URL。比如,讓我們假設應用中返回了一個文字為 "About Us" 的超鏈:

<a href="/about-us">About Us</a>

現在,讓我們編寫一個測試並斷言點選連結會跳轉到相應的頁面:

public function testBasicExample()
{
  $this->visit('/')
       ->click('About Us')
       ->seePageIs('/about-us')
}

與表單互動

Laravel 同樣也提供了多種方法來進行表單測試。typeselectcheckattach,和 press 方法允許你與所有的表單輸入進行互動。比如,讓我們來想象一下一個存在於註冊頁面中的表單:

<form action="/register" method="POST">
  {{ csrf_field() }}
  <div>
    Name: <input type="text" name="name">
  </div>
  <div><input type="checkbox" value="yes" name="terms">Accept Terms</input></div>

  <div><input type="submit"></div>
</form>

我們可以編寫一個測試來完成表單並檢查結果:

public function testNewUserRegistration()
{
  $this->visit('/register')
       ->type('Taylor', 'name')
       ->check('terms')
       ->press('Register')
       ->seePageIs('/dashboard');
}

當然,如果你的表單中包含了一些單選按鈕或者下拉框之類的其他輸入,你同樣可以輕鬆的進行填充這些型別的欄位。下面列出了所有的表單操作方法:

方法 描述
$this->type($text, $elementName) 對給定的欄位進行輸入
$this->select($value, $elementName) 選中一個單選按鈕或者下拉框
$this->check($elementName) 選中核取方塊
$this->uncheck($elementName) 取消選中核取方塊
$this->attach($pathToFile, $elementName) 附加一個檔案到表單
$this->press($buttonTextOrElementName) 單擊給定的文字或名稱的按鈕

與附件互動

如果你的表單中包含了 file 輸入型別,你可以使用 attach 方法來附加附件:

public function testPhotoCanBeUploaded()
{
  $this->visit('/upload')
       ->type('File Name', 'name')
       ->attach($absolutePathToFile, 'photo')
       ->press('Upload')
       ->see('Upload Successful!');
}

測試 JSON APIs

Laravel 同樣也對測試 JSON APIs 和它們的響應提供了多種幫助方法。比如,getpostputpatch,和 delete 方法可以用來釋出一個相應 HTTP 行為方式的請求。你可以輕鬆的傳遞資料和頭資訊到這些方法中。在開始之前,讓我們來編寫一個 POST 請求的測試來斷言 /user 會返回給定陣列的 JSON 格式:

<?php

class ExampleTest extends TestCase
{
  /**
   * A basic functional test example.
   *
   * @return void
   */
   public function testBasicExample()
   {
     $this->json('POST', '/user', ['name' => 'Sally'])
          ->seeJson([
            'created' => true,
          ]) ;
   }
}

seeJson 方法會轉換給定的陣列為 JSON,並且會驗證應用響應的完整 JSON 中是否會出現相應的片段。所以,如果響應中還含有其他 JSON 屬性,那麼這個測試依然會被通過。

驗證完全匹配的 JSON

如果你希望驗證完整的 JSON 響應,你可以使用 seeJsonEquals 方法,除非 JSON 相應於所給定的陣列完全匹配,否則該測試不會被通過:

<?php

class ExampleTest extends TestCase
{
  /**
   * A basic functional test example.
   *
   * @return void
   */
   public function testBasicExample()
   {
     $this->json('POST', '/user', ['name' => 'Sally'])
          ->seeJsonEquals([
            'created' => true,
          ]);
   }
}

驗證匹配 JSON 結構

驗證 JSON 響應是否採取給定的結構也是可以的。你可以使用 seeJsonStructure 方法並傳遞巢狀的鍵列表:

<?php

class ExampleTest extends TestCase
{
  /**
   * A basic functional test example.
   *
   * @return void
   */
   public function testBasicExample()
   {
     $this->get('/user/1')
          ->seeJsonStructure([
            'name',
            'pet' => [
              'name', 'age'
            ]
          ]);
   }
}

在上面的例子中表明瞭期望獲取一個含有 namepet 屬性的 JSON,並且 pet 鍵是一個含有 nameage 屬性的物件。如果含有額外的鍵,seeJsonStructure 方法並不會失敗。比如,如果 pet 還含有 weight 屬性,那麼測試依然會被通過。

你可以使用 * 來斷言所返回的 JSON 結構中的每一項都應該包含所列出的這些屬性:

<?php

class ExampleTest extends TestCase
{
  /**
   * A basic functional test example.
   *
   * @return void
   */
   public function testBasicExample()
   {
     // Assert that each user in the list has at least an id, name and email attribute.
     $this->get('/users')
          ->seeJsonStructure([
            '*' => [
              'id', 'name', 'email'
            ]
          ]);
   }
}

你也可以巢狀使用 *,下面的例子中,我們斷言 JSON 響應返回的每一個使用者都應該包含所列出的屬性,並且 pet 屬性應該也包含所給定的屬性:

$this->get('/users')
     ->seeJsonStructure([
       '*' => [
         'id', 'name', 'email', 'pets' => [
           '*' => [
             'name', 'age'
           ]
         ]
       ]
     ]);

會話 / 認證

Laravel 為測試期間的會話提供了多種幫助方法。首先,你需要通過 withSession 方法來根據給定的陣列設定 session 資料,這通常用來在測試應用的請求到在之前先設定一些 session 資料:

<?php

class ExampleTest extends TestCase
{
  public function testApplication()
  {
    $this->withSession(['foo' => 'bar'])
         ->visit('/');
  }
}

當然,會話常見的用途就是保留使用者的狀態,比如使用者認證。actingAs 幫助方法提供了一種方式來使用給定的使用者作為已認證的當前使用者。比如,我們可以使用模型工廠來生成一個認證使用者:

<?php

class ExampleTest extends TestCase
{
  public function testApplication()
  {
    $user = factory(App\User::class)->create();

    $this->actingAs($user)
         ->withSession(['foo' => 'bar'])
         ->visit('/')
         ->see('Hello' . $user->name);
  }
}

你也可以傳遞一個給定的守衛名稱到 actingAs 方法的第二個引數來選擇使用者認證的守衛:

$this->actingAs($user, 'backend')

禁用中介軟體

當測試應用時,你可以方便的在測試中禁中介軟體。這使你可以隔離的測試路由和控制器而免除中介軟體的顧慮。你可以簡單的引入 WithoutMiddleware trait 來在測試類中禁用所有的中介軟體:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
  use WithoutMiddleware;

  //
}

如果你希望只在個別的測試方法中禁用中介軟體,你可以在方法中使用 withoutMiddleware 方法:

<?php

class ExampleTest extends TestCase
{
  /**
   * A basic functional test example.
   *
   * @return void
   */
   public funcion testBasicExample()
   {
     $this->withoutMiddleware();

     $this->visit('/')
          ->see('Laravel 5');
   }
}

定製化 HTTP 請求

如果你希望構造一個自定義的 HTTP 請求,並且返回完整的 Illuminate\Http\Response 物件,你可以使用 call 方法:

public function testApplication()
{
  $response = $this->call('GET', '/');

  $this->assertEquals(200, $response->status());
}

如果你構造 POSTPUT,或者 PATCH 請求,你可以傳遞一個陣列來作為請求的輸入資料。當然,這些資料可以通過 Request 例項來在你的路由和控制器中可用:

$response = $this->call('POST', '/user', ['name' => 'Taylor']);

PHPUnit 斷言

Laravel 對 PHPUnit 測試提供了多種額外的斷言方法:

方法 描述
->assertResponseOk(); 斷言客戶端響應會返回 OK 狀態碼
->assertResponseStatus($code) 斷言客戶端響應給定的狀態碼
->assertViewHas($key, $value = null); 斷言響應的檢視中是否含有給定的資料片段。
->assertViewHasAll(array $bindings); 斷言檢視中是否含有所有的繫結資料
->assertViewMissing($key); 斷言檢視中是否缺失所給定的片段
->assertRedirectedTo($uri, $with = []); 斷言客戶端是否重定向到給定的 URI
->assertRedirectedToRoute($name, $parameters = [], $with = []); 斷言客戶端是否重定向到給定的動作
->assertSessionHas($key, $value = null); 斷言會話中是否含有給定的鍵
->assertSessionHasAll(array $bindings); 斷言會話中是否包含給定列表的資料
->assertSessionHasErrors($bindings = [], $format = null); 斷言會話中是否包含錯誤資料
->assertHasOldInput(); 斷言會話中是否包含舊的輸入
->assertSessionMissing($key); 斷言會話中是否缺失給定的鍵

與資料庫協作

Laravel 也提供了各種有用的工具來使測試基於資料庫驅動的應用更簡單。首先,你可以使用 seeInDatabase 方法來斷言給定的規範是否能在資料庫中匹配到相應的資料。比如,如果你希望驗證 users 表中是否含有 emailsally@example.com 的記錄,那麼你可以這麼做:

public function testDatabase()
{
  // Make call to application...

  $this->seeInDatabase('users', ['email' => 'sally@example.com']);
}

當然,類似 seeInDatabase 等方法僅僅是為了測試的方便。你可以自由的使用任意的 PHPUnit 自帶的斷言方法來補充你的測試。

在每個測試後重置資料庫

我們通常要在每次測試後還原資料庫,以便之前的測試資料不會對隨後的測試產生干擾。

使用遷移

一種選擇是在下個測試之前進行資料庫回滾和遷移操作。Laravel 提供了簡潔的 DatabaseMigrations trait 來自動的處理這些,你只需要在測試類引入該性狀:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
  use DatabaseMigrations;

  /**
   * A basic functional test example.
   *
   * @return void
   */
   public function testBasicExample()
   {
     $this->visit('/')
          ->see('Laravel 5');
   }
}

使用事務

另外一種選擇就是在每個測試用例中都使用資料庫的事務進行包裝,這一次,Laravel 提供了方便的 DatabaseTransactions trait 來自動的處理這些:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
  use DatabaseTransactions;

  /**
   * A basic functional test example.
   *
   * @return void
   */
   public function testBasicExample()
   {
     $this->visit('/')
          ->see('Laravel 5');
   }
}

注意: 這一性狀只會包裝預設資料庫連線的事務。

模型工廠

當測試時,通常需要在執行測試之前在資料庫中新增一些測試所需要的資料記錄。Laravel 允許你使用 "factories" 來對你的 Eloquent 模型定義一些預設的屬性設定來取代這些手動的新增資料的行為。在開始之前,我們來看一下應用的 database/factories/ModelFactory.php 檔案。該檔案中只包含了一個工廠的定義:

$factory->define(App\User::class, function (Faker\Generator $faker) {
  return [
    'name' => $faker->name,
    'email' => $faker->email,
    'password' => bcrypt(str_random(10)),
    'remember_token' => str_random(10),
  ] ;
});

在工廠定義中的閉包中,你可以返回一些模型中的用作測試的預設值。這個閉包會接收一個 Faker PHP 類庫的例項,它會幫你生成一些隨機資料用於測試。

當然,你可以自由的在 ModelFactory.php 檔案中新增額外的工廠。你也可以建立額外的工廠檔案來有效的組織管理。比如,你可以在 database/factories 目錄下建立一個 UserFactory.php 和 一個 CommentFactory.php 檔案。

多個工廠型別

有時候,你可能希望在同一個 Eloquent 模型上有多個工廠型別。比如,可能你希望除了普通使用者之外還有一個管理員的工廠。你可以使用 defineAs 方法來定義這些工廠:

$factory->defineAs(App\User::class, 'admin', function ($faker) {
  return [
    'name' => $faker->name,
    'email' => $faker->email,
    'password' => str_random(10),
    'remember_token' => str_random(10),
    'admin' => true,
  ];
});

你可以使用 raw 方法來檢索基礎工廠的屬性,這樣可用於屬性的複用,然後合併新增你所需要的額外屬性:

$factory->defineAs(App\User::class, 'admin', function ($faker) use ($factory) {
  $user = $factory->raw(App\User::class);

  return array_merge($user, ['admin' => true]); 
});

在測試中使用工廠

一旦你定義了工廠,你就可以在你的測試檔案或者資料庫 seed 檔案中使用全域性幫助函式 factory 工廠方法來生成模型的例項。那麼,讓我們來看一些建立模型的示例。首先,我們將使用 make 方法,它將會建立一些模型但是卻不會將其儲存到資料庫:

public function testDatabase()
{
  $user = factory(App\User::class)->make();

  // Use model in tests...
}

你可以通過傳遞一個陣列到 make 方法中來覆蓋模型中的預設值。只有指定的部分會被替換掉,而其它部分都會被保留為工廠定義的預設值:

$user = factory(App\User::class)->make([
  'name' => 'Abigail',
]);

你也可以根據給定的型別建立多個模型的集合:

// Create three App\User instances...
$user = factory(App\User::class, 3)->make();

// Create an App\User "admin" instance...
$user = factory(App\User::class, 'admin')->make();

// Create three App\User "admin" instance...
$user = factory(App\User::class, 'admin', 3)->make();

工廠模型持久化

create 方法不僅僅會建立一個模型例項,它還會使用 Eloquent 的 save 方法將其儲存到資料庫中:

public function testDatabase()
{
  $user = factory(App\User::class)->create();

  // Use model in tests...
}

這一次,你也可以傳遞一個陣列到 create 方法中來覆蓋預設的屬性值:

$user = factory(App\User::class)->create([
  'name' => 'Abigail',
]);

為模型新增關係

你甚至可以持久化多個模型到資料庫中。在這個例子中,我們甚至會為建立的模型附加一個關聯模型。當使用 create 方法來建立多個模型時,會返回一個集合例項,這允許你使用集合例項的方法,比如 each :

$users = factory(App\User::class, 3)
           ->create()
           ->each(function ($u) {
             $u->posts()->save(factory(App\Post::class)->make());
           });

關聯 & 屬性閉包

你也可以在工廠定義內使用一個閉包來附加關係模型,比如,如果你希望建立一個 Post 時也建立一個關聯的 User 例項,你可以這麼做:

$factory->fefine(App\Post::class, function($faker) {
  return [
    'title' => $faker->title,
    'content' => $faker->paragraph,
    'user_id' => function () {
      return factory(App\User::class)->create()->id;
    }
  ];
});

這些閉包還會接收工廠所包含的評估屬性陣列:

$factory->define(App\Post::class, function ($faker) {
  return [
    'title' => $faker->title,
    'content' => $faker->paragraph,
    'user_id' => function () {
      return factory(App\User::class)->create()->id;
    },
    'user_type' => function (array $post) {
      return App\User::find($post['user_id'])->type;
    }
  ]; 
});

模擬

模擬事件

如果你在 Laravel 中大量使用了事件系統,那麼你可能會想在進行測試時不觸發事件或者模擬執行一些事件。比如,如果你測試使用者註冊,你可能並不想觸發所有 UserRegistered 事件,因為這些可能會傳送電子郵件等。

Laravel 提供了方便的 expectsEvents 方法來驗證預期的事件是否觸發,但是會避免這些事件的真正的執行:

<?php

class ExampleTest extends TestCase
{
  public function testUserRegistration()
  {
    $this->expectsEvents(App\Events\UserRegistered::class);

    // Test user registration...
  }
}

你也可以使用 doesntExpectEvents 方法來驗證給定的事件沒有被觸發:

<?php

class ExampleTest extends TestCase
{
  public function testPodcastPurchase()
  {
    $this->expectsEvents(App\Events\PodcastWasPurchased::class);

    $this->doesntExpectEvents(App\Events\PaymentWasDeclined::class);

    // Test purchasing podcast...
  }
}

如果你需要避免所有的事件觸發,你可以使用 withoutEvents 方法:

<?php

class ExampleTest extends TestCase
{
  public function testUerRegistration()
  {
    $this->withoutEvents();

    // Test user registration code...
  }
}

模擬任務

有時候,你想在構建請求到應用時,簡單的測試一下控制器中是否進行了指定任務的分發。這可以使你隔離測試你的路由 / 控制器和你的任務邏輯,當然,你可以再寫一個測試用例來單獨的測試任務的執行。

Laravel 提供了方便的 expectsJobs 方法來驗證期望的任務是否被分發,但是任務並不會被真正的分發執行:

<?php

class ExampleTest extends TestCase
{
  public function testPurchasePodcast()
  {
    $this->expectsJobs(App\Jobs\PurchasePodcast::class);

    // Test purchase podcast code...
  }
}

注意:這個方法僅用來測試通過 DispatchesJobs trait 的分發或者 dispatch 幫助方法的分發。它並不檢測直接使用 Queue::push 釋出的任務。

模擬假面

當測試時,你或許經常需要模擬一個 Laravel 假面的呼叫。比如,考慮一下下面的控制器操作:

<?php

namespace App\Http\Controllers;

use Cache;

class UserController extends Controller
{
  /**
   * Show a list of all users of the application
   *
   * @return Response
   */
   public function index()
   {
     $value = Cache::get('key');

     //
   }
}

我們可以使用 shouldReceive 方法來模擬呼叫 Cache 假面,它會返回一個 Mockery 的模擬例項。由於假面實際上是通過 Laravel 的服務容器來解析和管理的,所以它們比一般的靜態類更具可測性。比如,讓我們來模擬 Cache 假面的呼叫:

<?php

class FooTest extends TestCase
{
  public function testGetIndex()
  {
    Cache::shouldReceive('get')
             ->once()
             ->with('key')
             ->andReturn('value');

    $this->visit('/users')->see('value');
  }
}

注意:你不應該模擬 Request 假面。你應該在執行測試時使用 HTTP 幫助方法如 callpost 來傳遞你所希望的輸入。

相關文章