Controller
建立 Controller 與 View :
$ php artisan make:controller ArticleController --plain
$ mkdir -p resources/views/articles
$ touch resources/views/articles/index.blade.php
如果不加 --plain
,會預設加入 index
、 create
、 store
... 等操作 resource 的相關方法。
在 ArticleController.php
中加入:
public function index()
{
$articles = [];
return view('articles.index', compact('articles');
}
編輯 app/Http/routes.php
,將已定義的 routes 暫時註解掉,再加入:
Route::resource('articles', 'ArticleController');
用 php artisan route:list
確認有沒有正確加入。
建立 Controller 測試
- 測試流程邏輯
- 測試 HTTP 狀態
把原來的 tests/ExampleTest.php
改名:
$ mv tests/ExampleTest.php tests/ArticleControllerTest.php
編輯 tests/ArticleControllerTest.php
:
class ArticleControllerTest extends TestCase {
public function testArticleList()
{
// 用 GET 方法瀏覽網址 /post
$this->call('GET', '/posts');
// 改用 Laravel 內建方法
// 實際就是測試是否為 HTTP 200
$this->assertResponseOk();
// 應取得 articles 變數
$this->assertViewHas('articles');
}
}
測試成功。
- 透過
call
方法執行 route,進而建立 Controller 實體來測試。 - Laravel 有內建一些測試 response 狀態的 assert 方法。
- session 或 cache 直接使用 array
注入 Repository 到 Controller 中
文章實際會從 ArticleRepository
裡取得,所以 Controller 會需要注入 Repository 。
namespace App\Http\Controllers;
use App\Repositories\ArticleRepository;
use Illuminate\Http\Response;
class ArticleController extends Controller {
protected $repository;
// 利用 Service Container (DI) 來自動注入 ArticleRepository
public function __construct(ArticleRepository $repository)
{
$this->repository = $repository;
}
// ...
}
修改 ArticleController::index
:
public function index()
{
// 改成從 ArticleRepository 中取得資料
$articles = $this->repository->latest10();
return view('article.index', compact('articles'));
}
測試不成功,因為我們沒有連接資料庫。
用 Mockery 隔離 ArticleRepository
安裝 Mockery :
$ composer require mockery/mockery --dev
- 不讓 Controller 測試接觸資料庫或其他需要 IO 的媒介
- 利用 Mockery 透過
ArticleRepository
生成假物件 (mock object) - 利用 Service Container 注入假物件取代原本應該被呼叫的物件
- 讓假物件的方法回傳假值
class ArticleControllerTest extends TestCase {
protected $repositoryMock = null;
public function setUp()
{
parent::setUp();
// Mockery::mock 可以利用 Reflection 機制幫我們建立假物件
$this->repositoryMock = Mockery::mock('App\Repositories\ArticleRepository');
// Service Container 的 instance 方法可以讓我們
// 用假物件取代原來的 ArticleRepository 物件
$this->app->instance('App\Repositories\ArticleRepository', $this->repositoryMock);
}
public function tearDown()
{
// 每次完成 test case 後,要清除掉被 mock 的假物件
Mockery::close();
}
public function testArticleList()
{
// 確認程式會呼叫一次 ArticleRepository::latest10 方法
// 實際上是為這個 mock object 加入 latest10 方法
// 沒有呼叫到的話就會發出異常
// 再假設它會回傳 foo 這個字串
// 這樣就不需要真的去連結資料庫
$this->repositoryMock
->shouldReceive('latest10')
->once()
->andReturn([]);
$this->call('GET', '/');
$this->assertResponseOk();
// 應取得 articles 變數
// 而其值為空陣列
$this->assertViewHas('articles', []);
}
}
新增資料的測試
- 程式中會透過 Repository 來新增資料,所以也要 mock 新增方法。
- 因為有用到 POST 方法,所以要考慮 CSRF
- 新增完成後要導向列表頁
先確認 CSRF 的保護機制是有作用的:
public function testCsrfFailed()
{
// 模擬沒有 token 時
// 程式應該是輸出 500 Error
$this->call('POST', 'articles');
$this->assertResponseStatus(500);
}
加入 ArticleControllerTest::testCreateArticleSuccess
:
use Illuminate\Support\Facades\Session;
class ArticleControllerTest extends TestCase {
// ...
// 測試新增資料成功時的行為
public function testCreateArticleSuccess()
{
// 會呼叫到 ArticleRepository::create
$this->repositoryMock
->shouldReceive('create')
->once();
// 初始化 Session ,因為需要避免 CSRF 的 token
Session::start();
// 模擬送出表單
$this->call('POST', 'articles', [
'title' => 'title 999',
'body' => 'body 999',
'_token' => csrf_token(), // 手動加入 _token
]);
// 完成後會導向列表頁
$this->assertRedirectedToRoute('articles.index');
}
完成新增功能,也就是 store
方法。而 store
方法也會透過 Service Container 來注入 HTTP Request 物件。
use Illuminate\Http\Request;
class ArticleController extends Controller {
// ...
/**
* Store a newly created resource in storage.
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
// 直接從 Http\Request 取得輸入資料
$this->repository->create($request->all());
// 導向列表頁
return Redirect::route('articles.index');
}