前言
在試著將PHPUnit導入Laravel Project之後遇到的第一個問題是,要測試什麼?
對於一般的Web application來說,最大宗的動作所在就是對於資料庫的新增、讀取、修改和刪除,再加上一些邏輯判斷跟動線的串接。而基本的資料庫存取操作,除非要到一些較客製化的動作,否則將Model Create好之後,不必再多寫任何一行code就可以對table進行操作,我的意思是,所以資料庫操作的層面(Model)也沒什麼好測試的了。
那到底對一個功能不複雜不特殊的Laravel web application而言,最大宗的邏輯會存在於哪裡?我認為是答案是Contoller中。
Controller的Request該怎麼來?
開始要用PHPUnit測試Laravel Controller時,遇到的第一個問題是,那input的Request要怎麼來?
光這個問題就卡了很久,若彙整一下從Google大神那問來的資料,大致上可以分為以下幾個方向:
- 直接送HTTP Request,讓資料跑過Routing再到達Controller,Request自然就出現
- 用mock來處理
- 手動new一個 \Illuminate\Http\Request object出來
想了一下之後,覺得直接送HTTP Request的方法是最貼近現實狀況,而且測試code寫起來似乎也是最簡潔簡單的,所以就直接選定了這個方法,其餘兩個方法暫且就沒有再多研究。
至於使用方法,可以看我為大家寫的實際範例應該就很清楚。
先看 CustomerController 的程式碼,裡面有 show、store、destroy 三個 methods,是對 Customers 這個Model做操作:
class CustomerController extends Controller
{
public function show($id)
{
return view('customer', [
'customer' => Customers::find($id)
]);
}
public function store(Request $request)
{
$customer = new Customers();
$customer->name = $request->name;
$customer->phone = $request->phone;
$customer->save();
return redirect()->action([CustomerController::class, 'show'], ['id' => $customer->id]);
}
public function destroy($id)
{
$customer = Customers::find($id);
$customer->delete();
return view('welcome');
}
}
再來是 CustomerControllerTest 的程式碼,分別對應測試 CustomerController 的三個 methods:
class CustomerControllerTest extends TestCase
{
protected static $customerIDs = [];
public function testStore()
{
// 打HTTP Post,去request CustomerController store method (儲存一筆資料)
$response = $this->postJson(action([CustomerController::class, 'store']), [
'name' => 'name-A',
'phone' => '0911123456'
]);
// 確認store method是return 302 redirect無誤
$response->assertStatus(302);
// 取最新insert的customer id
// 因為最新的customer id被store method帶在redirect url中,所以這裡從url中拆解出來
$redirectUrlExploded = explode('/', $response->headers->get('location'));
$customerID = array_pop($redirectUrlExploded);
// 確認customer id都是數字無誤
$this->assertStringMatchesFormat('%d', $customerID);
// 確認insert的資料內容無誤
$customer = Customers::find($customerID);
$this->assertSame('name-A', $customer->name);
$this->assertSame('0911123456', $customer->phone);
// 將customer id存到 self::$customerIDs 中,後續的test會需要
self::$customerIDs[] = $customerID;
}
/**
* @depends testStore
*/
public function testShow()
{
// 從 self::$customerIDs 取回customer id
// 因為測試動作是有連貫性而非獨立的,所以customer id才需要互相傳遞
$customerID = self::$customerIDs[0];
// 打HTTP Get,去request CustomerController show method (顯示一筆資料)
$response = $this->get(action([CustomerController::class, 'show'], ['id' => $customerID]));
// 確認HTTP status 200無誤
$response->assertOk();
// 確認view為"customer"無誤
$response->assertViewIs('customer');
// 確認會傳遞$customer給view,而其內容應該就是要等於剛剛 testStore() 中儲存的無誤
$customer = Customers::find($customerID);
$response->assertViewHas('customer', $customer);
}
/**
* @depends testShow
*/
public function testDestroy()
{
$customerID = self::$customerIDs[0];
// 打HTTP Get(Delete),去request CustomerController destroy method (刪除一筆資料)
$response = $this->deleteJson(action([CustomerController::class, 'destroy'], ['id' => $customerID]));
// 確認HTTP status 200無誤
$response->assertOk();
// 確認view為"welcome"無誤
$response->assertViewIs('welcome');
// 確認此筆資料確實已被刪除無誤
$customer = Customers::find($customerID);
$this->assertNull($customer);
}
}
以上是 Laravel 8.x 的作法,若是較舊的版本基本上概念是相同的,只不過因為版本的關係程式碼需要改過才能成功運作。有機會我再補上舊版本的程式碼範例。
想要實際上跑跑看,或看更完整程式碼的,可以到我的GitHub上把整個Laravel Project抓下來。
這個Test的是連貫動作,一個牽著一個的。以人的角度來看就是,首先插入一筆資料看是否正確(testStore),接著再看剛插入的資料是否正常讀取顯示(testShow),最後再試試能否正確把這筆資料正確刪除(testDestroy)。
所以test function需要 @depends 去標示彼此是如何串聯的,另外還需要 self::$customerIDs 在 test functions 之間互相傳遞測試資料。我都把unit test的每一行在做什麼盡量都寫註解在旁了,所以應該算不難理解。
議題
這樣的測試方法,其實已經包含了HTTP Request以及Routing,也就是說雖然貼近實際運作的狀況,不過這個「Unit」已經不「Unit」了。
如果以Laravel對於Test的分類來說,這個已經從Unit偏向Feature,但就我看又還沒真正到Feature,所以有點卡在一個尷尬的定位點。
但是如果因為定位含糊不清的問題,因而捨棄掉這樣的方法不用,我個人是覺得有點可惜,而且成本有點高,畢竟這樣的測試code真的是直覺、好懂又不拐彎抹角。
不過,要是因為測試的涵蓋範圍過大導致問題不容易點出,例如test controller fail但實際上是routung出錯導致的fail。如果是這類問題過於嚴重,我就偏向於不採用此篇文章所說的方法,讓「Unit」可以真正是「Unit」。
不知道大家的看法如何?又或是你有更好的做法,都歡迎提出討論。
- 旅遊攝影| 探訪山中特色小鎮 – 飛驒古川 - 2023-08-18
- 日本名古屋 熱田神宮|多啦A夢的任意門,自由穿梭在城市與山林之間 - 2023-08-13
- 《JOKER電影交響音樂會》 心得紀錄 - 2022-03-13