測試

簡介

Laravel 在建立時就已考慮到測試的部分。事實上,預設就支援以 PHPUnit 做測試,而且已經為你的應用程式建立了 phpunit.xml 檔案。框架還提供了一些便利的輔助方法,讓你更直觀的測試你的應用程式。

tests 目錄中有提供一個 ExampleTest.php 的範例檔案。安裝新的 Laravel 應用程式之後,只要在命令列上執行 phpunit,就可以進行測試。

測試環境

在執行測試時,Laravel 會自動將環境變數設定為 testing,並將 Session 及快取存入陣列形式,也就是說在測試時不會有任何 Session 或快取資料被儲存。

你可以自由建立其他必要的測試環境設定。testing 的環境變數可以在 phpunit.xml 檔案中做修改。

定義並執行測試

要建立一個測試案例,使用 make:test Artisan 指令:

php artisan make:test UserTest

此指令會放置一個新的 UserTest 類別至你的 tests 目錄。接著就可以像平常使用 PHPUnit 一樣定義測試方法。要執行測試只需要在終端機上執行 phpunit 指令:

<?php

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

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

注意: 如果你要在你的類別定義自己的 setUp 方法,請確定有呼叫 parent::setUp

測試應用程式

Laravel 對於產生 HTTP 請求並送至應用程式,檢查輸出,甚至填寫表單,都提供了非常流利的 API。 舉例來說,你可以看看 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 方法則斷言在應用程式回傳的回應中,有指定的文字。The dontSee method asserts that the given text is not returned in the application response.這是 Laravel 所提供最基本的應用程式測試。

與你的應用程式進行互動

當然,除了斷言文字是否存在於一個給定的回應,你可以做更多的互動。讓我們看看點擊連結及填寫表單的範例:

點擊連結

在這個測試中,我們會產生一個請求送到應用程式,並「點擊」回傳回應中的連結,接著斷言我們會停留在指定的 URI。舉個例子,假設在回應中有個連結,並寫著「About Us」:

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

現在,我們撰寫一個測試,點擊連結並斷言使用者會停留在正確的頁面:

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

使用表單

Laravel 還提供了幾種用於測試表單的方法。透過 typeselectcheckattachpress 方法讓你與表單所有的輸入欄進行互動。舉個例子,讓我們想像一下有個表單在應用程式的註冊頁面:

<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
    </div>

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

我們可以撰寫一個測試來填寫此表單,並檢查結果:

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

當然,如果你的表單包含像是單選欄或下拉式選單的其他輸入欄,你也可以很輕鬆的填入這些類型的區域。以下是每個表單操作方法的列表:

方法 說明
$this->type($text, $elementName) 「輸入(type)」文字在一個給定的區域
$this->select($value, $elementName) 「選擇(select)」一個單選欄或下拉式選單的區域
$this->check($elementName) 「勾選(Check)」一個複選欄的區域
$this->attach($pathToFile, $elementName) 「附上(Attach)」一個檔案至表單
$this->press($buttonTextOrElementName) 「按下(Press)」一個指定文字或名稱的按鈕

使用附件

如果你的表單包含 file 的輸入欄類型,可以使用 attach 方法附上檔案:

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

測試 JSON APIs

Laravel 也提供了幾個輔助方法測試 JSON APIs 及其回應。舉例來說,getpostputpatchdelete 方法可以用於發出各種 HTTP 動詞的請求。你也可以很輕鬆的傳入資料或是標頭到這些方法。首先,我們撰寫一個測試,發出 POST 請求至 /user ,並斷言會回傳JSON 格式的指定陣列:

<?php

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

seeJson 方法會將傳入的陣列轉換成 JSON,並驗證該 JSON 片段是否存在於應用程式回傳的 JSON 回應中的任何位置。也就是說,即使有其他的屬性在 JSON 回應中,但是只要給定的片段存在,此測試仍然會通過。

驗證 JSON 完全匹配

如果你想驗證傳入的陣列要與應用程式回傳的 JSON 完全匹配,可以使用 seeJsonEquals 方法:

<?php

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

Sessions 和認證

Laravel 提供了幾個輔助方法在測試時使用 Session。首先,你需要設定 Session 資料至給定的陣列,並使用 withSession 方法。對於要測試送到應用程式的請求之前,先將資料載入 session 上相當有用:

<?php

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

當然,一般使用 Session 時都是用於保持使用者的狀態,像是認證使用者。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);
    }
}

停用中介層

測試應用程式時,你會發現,在某些測試中停用中介層是很方便的。讓你可以隔離任何中介層的影響,來測試路由及控制器。Laravel 包含一個簡潔的 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 function 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 請求,你可以在請求時傳入一個陣列作為輸入資料。當然,你可在你的路由及控制器中透過請求實例取用這些資料:

   $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 = []); 斷言客戶端是否被重導至給定的路由
->assertRedirectedToAction($name, $parameters = [], $with = []); 斷言客戶端是否被重導至給定的行為。
->assertSessionHas($key, $value = null); 斷言 session 中有給定的值。
->assertSessionHasAll(array $bindings); 斷言 session 中有給定的列表值。
->assertSessionHasErrors($bindings = [], $format = null); 斷言 session 有錯誤的綁定。
->assertHasOldInput(); 斷言 session 有舊輸入資料。

使用資料庫

Laravel 也提供了多種有用的工具,讓你更容易測試使用資料庫的應用程式。首先,你可以使用 seeInDatabase 輔助方法,來斷言資料庫中是否存在與指定條件互相匹配的資料。舉例來說,如果我們想驗證 users 資料表中是否存在 email 值為 sally@example.com 的資料,我們可以按照以下的方式:

public function testDatabase()
{
    // 建立呼叫至應用程式...

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

當然,使用 seeInDatabase 方法及其他的輔助方法只是基於方便。你可以自由使用任何 PHPUnit 內建的斷言方法來擴充你的測試。

每次測試結束後重置資料庫

在每次測試結束後都重置你的資料是相當有用的,這樣前次的測試資料就不會干擾之後的測試。

使用遷移

其中一種方式就是在每次測試後都還原資料庫,並在下次測試前進行遷移。Laravel 提供了簡潔的 DatabaseMigrations trait,它會自動幫你處理這些操作。你只需要在測試類別中使用此 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');
    }
}

注意:此 trait 的交易只會包含預設的資料庫連接。

模型工廠

測試時,常常需要在執行測試之前寫入一些資料到資料庫中。建立測試資料時,除了手動設定每個欄位的值,Laravel 讓你可以使用 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 檔案。

多個工廠類型

有時你可能希望針對同一個 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]);
});

在測試中使用工廠

定義工廠後,就可以在測試或是資料庫填充檔案中,透過全域 factory 函式產生模型實例。那麼讓我們來看看幾個建立模型的例子。首先我們會使用 make 方法建立模型,但是不將它們儲存至資料庫:

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

    // 在測試中使用模型...
}

如果你想覆寫模型中的某些預設值,你可以傳遞一個包含數值的陣列至 make 方法。只有指定的數值會被替換,其它剩餘的數值則會按照工廠指定的預設值設定:

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

你也可以建立一個含有多個模型的集合,或建立一個給定類型的模型:

// 建立三個 App\User 實例...
$users = factory(App\User::class, 3)->make();

// 建立一個 App\User「管理員」實例...
$user = factory(App\User::class, 'admin')->make();

// 建立三個 App\User「管理員」實例...
$users = factory(App\User::class, 'admin', 3)->make();

保存工廠模型

create 方法不僅建立模型實例,也可以使用 Eloquent 的 save 方法將它們儲存至資料庫:

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

    // 在測試中使用模型...
}

同樣的,你可以在傳遞陣列至 create 方法時覆寫模型的屬性:

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

增加關聯至模型

你甚至能保存多個模型至資料庫。在本例中,我們還會增加關聯至我們建立的模型。當使用 create 方法建立多個模型時,它會回傳一個 Eloquent 集合實例,讓你能夠使用集合所提供的方便函式,像是 each

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

模擬

模擬事件

如果你正在頻繁地使用 Laravel 的事件系統,你可能希望在測試時停止或是模擬某些事件。舉例來說,如果你正在測試使用者註冊功能,你可能不希望所有 UserRegistered 事件的處理程序被執行,因為它們會發送「歡迎」的電子郵件等等。

Laravel 提供了簡潔的 expectsEvents 方法,驗證預期的事件有被執行,但是防止該事件的任何處理程序被執行:

<?php

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

        // 測試使用者註冊的程式碼...
    }
}

如果你希望防止所有事件的處理程序被執行,你可以使用 withoutEvents 方法:

<?php

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

        // 測試使用者註冊的程式碼...
    }
}

模擬任務

有時你可能希望當發出請求至你的應用程式時,簡單的測試由你的控制器所派送的任務。這麼做能夠使你隔離並測試你的路由或控制器-設定除了任務以外的邏輯。當然,你之後可以在一個單獨的測試案例測試該任務。

Laravel 提供了一個簡潔的 expectsJobs 方法,驗證預期的任務有被派送,但是任務本身不會被執行:

<?php

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

        // 測試購買 podcast 的程式碼...
    }
}

注意: 此方法只檢測被 DispatchesJobs trait 的派送方法所派送的任務。它並不會檢測直接發送到 Queue::push 的任務。

模擬 Facades

測試時,你可能時常需要模擬呼叫一個 Laravel facade。舉個例子,參考下方的控制器行為:

<?php

namespace App\Http\Controllers;

use Cache;
use Illuminate\Routing\Controller;

class UserController extends Controller
{
    /**
     * 顯示應用程式所有使用者的列表。
     *
     * @return Response
     */
    public function index()
    {
        $value = Cache::get('key');

        //
    }
}

我們可以透過 shouldReceive 方法模擬呼叫 Cache facade,它會回傳一個 Mockery 模擬的實例。因為 facades 實際上已經被 Laravel 服務容器解決及管理,它們比起一般的靜態類別更有可測試性。舉個例子,讓我們模擬我們呼叫 Cache facade:

<?php

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

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

注意: 你不應該模擬 Request facade。應該在測試時使用像是 callpost 的 HTTP 輔助方法來傳遞你想要的資料。