【Laravel5.2翻譯】單元測試


前陣子看了點Laravel源碼,越看越亂,網上大部分中文文檔都是直譯,比較生澀難懂,還是決定看英文文檔順便就我的理解做下翻譯整理記錄下來

思維導圖

簡介

Laravel構建的時候就帶上測試。實際上,內含支持PHPUnit的測試,開箱即用,同時已經為你應用創建了phpunit.xml文件。框架給你提供了方便的幫助方法,可以讓你形象地測試你應用。

tests文件夾 提供了 ExampleText.php 文件。安裝完Laravel應用,只要在命令行執行vendor/bin下的 phpunit就能運行你的測試

測試環境

當你運行測試,Laravel會自動為測試配置環境。測試的時候Laravel自動配置session和緩存到你的數組驅動,意味著測試的時候不會持久化session和緩存數據。
需要的話,你可以自由的創建其他測試環境。測試環境的參數可以在phpunit.xml文件里配置,但要在運行測試前確保你用config:clear Artisan命令清理的配置緩存。

定義&運行測試

make:testArtisan命令創建一個新的測試案例:

php artisan make:test UserTest

這個命令會在tests文件夾下創建一個新的UserTest。然后你就可以像平時用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提供了非常流暢的API,可以讓你向引用發送Http請求,檢測輸出,甚至填充表單。
例如,看下ExamleTest.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方法確保返回消息中沒有給定文本。這是Laravel可用的最基本的應用測試。

和應用交互

當然,比起確保文本出現在給定回復,你可以做更多。讓我們來看些點擊鏈接和填充表單的例子:

點擊鏈接
在這個測試中,我們將對應用發起請求,在返回的響應中“點擊”鏈接,然后確保登入指定URL。例如,我們假設在返回的響應 有一個文本為“About Us”的鏈接:

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

現在,讓我們來寫一個測試點擊鏈接確保用戶打開正確的頁面:

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

處理表單

Laravel也為測試表單提供了多重方法。type,select,check,attack,和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
    </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) 輸入文本
$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 API

Laravel也為測試JSON API和它們的響應提供了許多幫助。例如,get,post,put,patchdelete方法用來解決各種HTTP請求。你也可以輕松用這些方法傳遞數據和頭文件。首先,讓我們寫一個測試,對/user發送一個POST請求然后確保給定的數組在返回的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精準匹配
如果你像核實給定數組完全匹配應用返回的JSON,你可以用seeJsonEqual方法:

<?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'
                 ]
             ]);
    }
}

上面的例子期望收到一個name和一個包含nameage的嵌套對象pet。只要額外關鍵字帶響應中存在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'
                 ]
             ]);
    }
}

你可以在嵌套中使用*,這樣的話,你可以確保每個用戶的數據里都包含給定的屬性集同是每個pet屬性都包含給定屬性集:

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

Session/認證

Laravel為測試期間用session提供了一些幫助。首先,你可以用withSession方法把session的數據設置成指定數組。這個方法對請求前加載session很有用:

<?php

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

當然,seesion常見的應用就是保存用戶狀態,比如認證用戶。actingAs方法提供了一個認證給定用戶為當前用戶的簡便方法。比如,我們可以用model factory生成和認證一個用戶:

<?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);
    }
}

你也可以指定用哪個guard來認證給定用戶,通過給actingAs方法的第二個參數傳遞一個guard名字:

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

中間件不可用

在測試你的應用的時候,你會發現你可以很方便的在某些測試中讓中間件不可用。這將使你可以在隔離中間件的情況下測試你的路由和控制器。Laravel包含一個WithoutMiddlware 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請求,你可能需要傳送一個輸入數據的數組。當然,通過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 = []); 確認客戶端是否重定向到指定URL
->assertRedirectedToRoute($name, $parameters = [], $with = []); 確認客戶端是否重定向到指定路由
->assertRedirectedToAction($name, $parameters = [], $with = []); 確認客戶端是否重定向到指定動作
->assertSessionHas($key, $value = null); 確認Session里有給定值
->assertSessionHasAll(array $bindings); 確認Session中有給定值集合
->assertSessionHasErrors($bindings = [], $format = null); 確認Session有錯誤綁定
->assertHasOldInput(); 確認Session中有舊的輸入
->assertSessionMissing($key); 確認Session中缺失指定關鍵字

數據庫操作

Laravel提供了各種各樣有用的工具來簡化測試我們的數據庫驅動應用。首先,你可以用seeInDatabase來確認數據庫里是存在符合你條件的數據。例如,如果你像驗證在user表中有條數據它email值為sally@example.com,你可以這樣寫:

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

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

當然,seeInDatabase方法和其他類似的幫助方法都是為了方便。你可以自由使用任何PHPUnit的內建斷言方法來支持你的測試。

測試后重置數據庫

在測試后重置數據庫是很有用的,這讓后續測試不會受到前面測試的數據影響。

使用Migration

一個選擇就是每次測試完都回滾數據庫然后在下次測試前移植過去。Laravel提供了DatabaseMigrationstrait來自動進行這些操作:

<?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提供了一個方便的DatabaseTransactionstrait來自動操作這些:

<?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 models定義一個屬性集合,你就不用在你創建測試數據的時候手動指定每一列的值了。首先,讓我們看一下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),
    ];
});

在factory定義的閉包中,你可以返回模塊上的所有屬性的測試值。這個閉包會接受一個Faker PHP Library實例,它允許你很方便的生成各種隨機的數據來測試。

當然,你可以自由地在ModelFactory.php中添加自己的額外工廠。你也可以為每個model添加另外的工廠文件來更好的組織。比如,你可以在你的database/factories目錄下創建UserFactory.phpCommentFactory.php文件。

多樣工廠類型

有時候你像為同一個Eloquent model類創建多個工程。比如除了普通用戶你還想為管理員用戶添加工程。你可以用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方法在你的測試或數據庫seed文件中使用它們來生成model實例。讓我們看一些創建model的例子。首先,我們用make方法,它會創建model但不會存入數據庫:

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

    // Use model in tests...
}

如果你想重寫一些你model中的默認值,你可以給make方法傳遞一個數組。只有指定的值會被替換,其他的值都會保留你在工廠中定義的默認值:

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

你也可以創建一個model集合或者創建一個給定類型的model:

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

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

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

持久化工廠model
create方法不但創建model實例,還會用Eloquent的save方法把它們存入數據庫:

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

    // Use model in tests...
}

同樣的,你可以通過給create方法傳數組來重寫model屬性:

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

為Model添加關聯
你或許想保存多個model到數據庫。在這個例子中,你可以對一個已建model附加關聯。當你用create方法創建多個models,會返回一個Eloquent collection實例,它讓你可以用它提供的任何快捷函數,比如each

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

關聯和屬性閉包
你可以在工廠定義中用屬性閉包添加關聯。比如,如果你想在創建Post的時候創建一個User,你可以這樣做:

$factory->define(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事件操作被執行,因為它們會發送一個welcome電子郵件等等。
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 testUserRegistration()
    {
        $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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容