[TOC]
### **1、簡介**
[Laravel](http://laravelacademy.org/tags/laravel "View all posts in Laravel")?植根于[測試](http://laravelacademy.org/tags/%e6%b5%8b%e8%af%95 "View all posts in 測試"),實際上,內置使用[PHPUnit](https://phpunit.de/)對測試提供支持是即開即用的,并且`phpunit.xml`文件已經為應用設置好了。框架還提供了方便的輔助方法允許你對應用進行富有表現力的測試。
`tests`?目錄中提供了一個?`ExampleTest.php`?文件,安裝完新的 Laravel 應用后,只需簡單在命令行運行`phpunit`來運行測試。
#### **1.1 測試環境**
運行測試的時候,Laravel 自動設置配置環境為?`testing`。Laravel在測試時自動配置 session 和 cache 驅動為數組驅動,這意味著測試時不會持久化存儲 session 和 cache。
如果需要的話你也可以創建其它測試環境配置。`testing`?環境變量可以在?`phpunit.xml`?文件中配置。
#### **1.2 定義&運行測試**
要創建一個新的測試用例,可以使用如下Artisan命令:
~~~
php artisan make:test UserTest
~~~
該命令將會在?`tests`?目錄下生成一個新的?`UserTest`?類。然后你可以使用?[PHPUnit](http://laravelacademy.org/tags/phpunit "View all posts in 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`。
### **2、應用測試**
Laravel 為生成 HTTP?[請求](http://laravelacademy.org/tags/%e8%af%b7%e6%b1%82 "View all posts in 請求")、測試輸出、以及填充[表單](http://laravelacademy.org/tags/%e8%a1%a8%e5%8d%95 "View all posts in 表單")提供了平滑的API。舉個例子,我們看下?`tests`?目錄下包含的`ExampleTest.php`文件:
~~~
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase{
/**
* 基本功能測試示例
*
* @return void
*/
public function testBasicExample()
{
$this->visit('/')
->see('Laravel 5')
->dontSee('Rails');
}
}
~~~
`visit`?方法生成了一個GET請求,`see`?方法對我們從應用返回響應中應該看到的給定文本進行[斷言](http://laravelacademy.org/tags/%e6%96%ad%e8%a8%80 "View all posts in 斷言")。`dontSee`?方法對給定文本沒有從應用響應中返回進行斷言。在Laravel中這是最基本的有效應用測試。
#### **2.1 與應用交互**
當然,除了對響應文本進行斷言之外還有做更多測試,讓我們看一些點擊鏈接和填充表單的例子:
##### **點擊鏈接**
在本測試中,我們將為應用生成請求,在返回的響應中“點擊”鏈接,然后對訪問URI進行斷言。例如,假定響應中有一個“關于我們”的鏈接:
~~~
<a href="/about-us">About Us</a>
~~~
現在,讓我們編寫一個測試點擊鏈接并斷言用戶訪問頁面是否正確:
~~~
public function testBasicExample(){
$this->visit('/')
->click('About Us')
->seePageIs('/about-us');
}
~~~
##### **處理表單**
Laravel 還為處理表單提供了多個方法。`type`,?`select`,?`check`,?`attach`, 和`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)` | “Type” 文本到給定字段 |
| `$this->select($value, $elementName)` | “Select” 單選框或下拉列表 |
| `$this->check($elementName)` | “Check” 復選框 |
| `$this->attach($pathToFile, $elementName)` | “Attach” 文件到表單 |
| `$this->press($buttonTextOrElementName)` | “Press” ?給定文本或name的按鈕 |
| `$this->uncheck($elementName)` | “Uncheck”復選框 |
##### **處理附件**
如果表單包含`file`輸入類型,可以使用`attach`方法添加文件到表單:
~~~
public function testPhotoCanBeUploaded(){
$this->visit('/upload')
->name('File Name', 'name')
->attach($absolutePathToFile, 'photo')
->press('Upload')
->see('Upload Successful!');
}
~~~
#### **2.2 測試[JSON](http://laravelacademy.org/tags/json "View all posts in JSON")?API**
Laravel 還提供多個幫助函數用于測試 JSON API 及其響應。例如,`get`,?`post`,?`put`,?`patch`, 和?`delete`方法用于通過多種 HTTP 請求方式發出請求。你還可以輕松傳遞數據和頭到這些方法。作為開始,我們編寫測試來生成 POST 請求到?`/user`?并斷言返回的數據是否是 JSON 格式:
~~~
<?php
class ExampleTest extends TestCase{
/**
* 基本功能測試示例
*
* @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{
/**
* 基本功能測試示例
*
* @return void
*/
public function testBasicExample()
{
$this->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`?和嵌套?`pet`?對象(該對象包含?`name`?和?`age`?屬性)的 JSON 數據。如果 JSON 響應中包含其它額外鍵?`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'
]
]
]
]);
~~~
#### **2.3?Session/[認證](http://laravelacademy.org/tags/%e8%ae%a4%e8%af%81 "View all posts in 認證")**
Laravel提供了多個輔助函數用于在測試期間處理[Session](http://laravelacademy.org/post/3261.html),首先,可以使用`withSession`方法設置 session 值到給定數組。這在測試請求前獲取 session 數據時很有用:
~~~
<?php
class ExampleTest extends TestCase{
public function testApplication()
{
$this->withSession(['foo' => 'bar'])
->visit('/');
}
}
~~~
當然,session的通常用于操作用戶狀態,例如認證用戶。輔助函數`actingAs`?提供了認證給定用戶為當前用戶的簡單方法,例如,我們使用[模型工廠](http://laravelacademy.org/post/3274.html#model-factories)生成和認證用戶:
~~~
<?php
class ExampleTest extends TestCase{
public function testApplication()
{
$user = factory('App\User')->create();
$this->actingAs($user)
->withSession(['foo' => 'bar'])
->visit('/')
->see('Hello, '.$user->name);
}
}
~~~
還可以通過傳遞?`guard`?名稱作為?`actingAs`?函數的第二個參數的方式來指定使用哪個?`guard`?來認證給定用戶:
~~~
$this->actingAs($user, 'backend')
~~~
#### **2.4 禁止中間件**
測試應用時,為某些測試禁止[中間件](http://laravelacademy.org/post/2803.html)很方便。這種機制允許你將路由和控制器與中間件孤立開來做測試,Laravel包含了一個簡單的?`WithoutMiddleware`?trait,可以使用該 trait 自動在測試類中禁止所有中間件:
~~~
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase{
use WithoutMiddleware;
//
}
~~~
如果你只想在某些方法中禁止中間件,可以在測試方法中調用?`withoutMiddleware`?方法:
~~~
<?php
class ExampleTest extends TestCase{
/**
* 基本功能測試示例
*
* @return void
*/
public function testBasicExample()
{
$this->withoutMiddleware();
$this->visit('/')
->see('Laravel 5');
}
}
~~~
#### **2.5 自定義HTTP請求**
如果你想要在應用中生成自定義HTTP請求并獲取完整的?`Illuminate\Http\Response`?對象,可以使用?`call`?方法:
~~~
public function testApplication(){
$response = $this->call('GET', '/');
$this->assertEquals(200, $response->status());
}
~~~
如果你要生成`POST`,?`PUT`, 或者?`PATCH`請求可以在請求中傳入輸入數據數組,在路由或控制器中可以通過[Request實例](http://laravelacademy.org/post/2824.html)訪問請求數據:
~~~
$response = $this->call('POST', '/user', ['name' => 'Taylor']);
~~~
#### **2.6 PHPUnit斷言方法**
Laravel 為 PHPUnit 測試提供了額外的斷言方法:
| 方法 | 描述 |
| --- | --- |
| `->assertResponseOk();` | 斷言客戶端響應狀態碼是否為200 |
| `->assertResponseStatus($code);` | 斷言客戶端響應狀態碼是否是給定$code |
| `->assertViewHas($key, $value = null);` | 斷言響應視圖是否包含給定的綁定數據片段 |
| `->assertViewHasAll(array $bindings);` | 斷言視圖是否包含給定綁定數據列表 |
| `->assertViewMissing($key);` | 斷言響應視圖缺失綁定數據片段 |
| `->assertRedirectedTo($uri, $with = []);` | 斷言客戶端是否重定向到給定URI |
| `->assertRedirectedToRoute($name, $parameters = [], $with = []);` | 斷言客戶端是否重定向到給定路由 |
| `->assertRedirectedToAction($name, $parameters = [], $with = []);` | 斷言客戶端是否重定向到給定action |
| `->assertSessionHas($key, $value = null);` | 斷言session是否包含給定值 |
| `->assertSessionHasAll(array $bindings);` | 斷言session是否保護眼給定值列表 |
| `->assertSessionHasErrors($bindings = [], $format = null);` | 斷言session是否包含錯誤綁定 |
| `->assertHasOldInput();` | 斷言sessio包含上次輸入數據 |
### **3、處理[數據庫](http://laravelacademy.org/tags/%e6%95%b0%e6%8d%ae%e5%ba%93 "View all posts in 數據庫")**
Laravel還提供了多種有用的工具讓測試[數據庫](http://laravelacademy.org/post/2942.html)驅動的應用更加簡單。首先,你可以使用幫助函數?`seeInDatabase`?來斷言數據庫中的數據是否和給定數據集合匹配。例如,如果你想要通過 email 值為`sally@example.com`的條件去數據表`users`查詢是否存在該記錄 ,我們可以這樣做:
~~~
public function testDatabase(){
// 調用應用...
$this->seeInDatabase('users', ['email' => 'sally@foo.com']);
}
~~~
當然,`seeInDatabase`方法和其它類似輔助方法都是為了方便起見進行的封裝,你也可以使用其它PHPUnit內置的斷言方法來進行測試。
#### **3.1 每次測試后重置數據庫**
每次測試后重置數據庫通常很有用,這樣的話上次測試的數據不會影響下一次測試。
##### **使用遷移**
一種方式是每次測試后回滾數據庫并在下次測試前重新遷移。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;
/**
* 基本功能測試示例
*
* @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;
/**
* 基本功能測試示例
*
* @return void
*/
public function testBasicExample()
{
$this->visit('/')
->see('Laravel 5');
}
}
~~~
> 注意:該trait只在事務中封裝默認數據庫連接。
#### **3.2?[模型工廠](http://laravelacademy.org/tags/%e6%a8%a1%e5%9e%8b%e5%b7%a5%e5%8e%82 "View all posts in 模型工廠")**
測試時,通常需要在執行測試前插入新數據到數據庫。在創建測試數據時,Laravel允許你使用“factories”為每個[Eloquent模型](http://laravelacademy.org/post/2995.html)定義默認的屬性值集合,而不用手動為每一列指定值。作為開始,我們看一下`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),
];
});
~~~
在閉包中,作為工廠定義,我們返回該模型上所有屬性默認測試值。該閉包接收PHP庫[Faker](https://github.com/fzaninotto/Faker)實例,從而允許你方便地為測試生成多種類型的隨機數據。
當然,你可以添加更多工廠到`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',
]);
~~~
還可以創建多個模型集合或者創建給定類型的集合:
~~~
// 創建3個 App\User 實例...
$users = factory(App\User::class, 3)->make();
// 創建1個 App\User "admin" 實例...
$user = factory(App\User::class, 'admin')->make();
// 創建3個 App\User "admin" 實例...
$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集合](http://laravelacademy.org/post/3031.html)實例,從而允許你使用集合提供的所有便利方法,例如`each`:
~~~
$users = factory(App\User::class, 3)
->create()
->each(function($u) {
$u->posts()->save(factory(App\Post::class)->make());
});
~~~
### **4、[模擬](http://laravelacademy.org/tags/%e6%a8%a1%e6%8b%9f "View all posts in 模擬")**
#### **4.1 模擬事件**
如果你在重度使用Laravel的時間系統,可能想要在測試時模擬特定[事件](http://laravelacademy.org/post/3162.html)。例如,如果你在測試用戶注冊,你可能不想所有`UserRegistered`的時間處理器都被觸發,因為這可能會發送歡迎郵件,等等。
Laravel提供可一個方便的`expectsEvents`方法來驗證期望的事件被觸發,但同時阻止該事件的其它處理器運行:
~~~
<?php
class ExampleTest extends TestCase{
public function testUserRegistration()
{
$this->expectsEvents(App\Events\UserRegistered::class);
// 測試用戶注冊代碼...
}
}
~~~
可以使用`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();
// 測試用戶注冊代碼...
}
}
~~~
#### **4.2 模擬[隊列任務](http://laravelacademy.org/post/3252.html)**
有時候,你可能想要在請求時簡單測試控制器分發的指定任務,這允許你孤立的測試路由/控制器——將其從任務邏輯中分離出去,當然,接下來你可以在一個獨立測試類中測試任務本身。
Laravel提供了一個方便的`expectsJobs`方法來驗證期望的任務被分發,但該任務本身不會被測試:
~~~
<?php
class ExampleTest extends TestCase{
public function testPurchasePodcast()
{
$this->expectsJobs(App\Jobs\PurchasePodcast::class);
// 測試購買播客代碼...
}
}
~~~
> 注意:這個方法只檢查通過`DispatchesJobs`?trait分發方法分發的任務,并不檢查直接通過`Queue::push`分發的任務。
#### **4.3 模擬門面**
測試的時候,你可能經常想要模擬[Laravel門面](http://laravelacademy.org/post/2920.html)的調用,例如,看看下面的控制器動作:
~~~
<?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`門面的調用,該方法返回一個[Mockery](https://github.com/padraic/mockery)模擬的實例,由于門面通過Laravel[服務容器](http://laravelacademy.org/post/2910.html)解析和管理,它們比通常的靜態類更具有可測試性。例如,我們來模擬`Cache`門面的調用:
~~~
<?php
class FooTest extends TestCase{
public function testGetIndex()
{
Cache::shouldReceive('get')
->once()
->with('key')
->andReturn('value');
$this->visit('/users')->see('value');
}
}
~~~
> 注意:不要模擬`Request`門面,取而代之地,在測試時傳遞輸入到HTTP幫助函數如`call`和`post`。
- 序言
- 發行版本說明
- 升級指南
- 貢獻代碼
- 開始
- 安裝
- 配置
- Laravel Homestead
- 基礎
- HTTP 路由
- HTTP 中間件
- HTTP 控制器
- HTTP 請求
- HTTP 響應
- 視圖
- Blade 模板引擎
- 架構
- 一次請求的生命周期
- 應用目錄結構
- 服務提供者
- 服務容器
- 門面(Facades)
- 數據庫
- 起步
- 查詢構建器
- 遷移
- 填充數據
- Eloquent ORM
- 起步
- 關聯關系
- 集合
- 訪問器&修改器
- 序列化
- 服務
- 用戶認證
- 用戶授權
- Artisan Console
- 訂閱支付實現:Laravel Cashier
- 緩存
- 集合
- 集成前端資源:Laravel Elixir
- 加密
- 錯誤&日志
- 事件
- 文件系統/云存儲
- 哈希
- 輔助函數
- 本地化
- 郵件
- 包開發
- 分頁
- Redis
- 隊列
- Session
- Envoy Task Runner
- 任務調度
- 測試
- 驗證
- 新手入門指南
- 簡單任務管理系統
- 帶用戶功能的任務管理系統