เขียน Feature Test ใน Laravel สำหรับเชื่อมต่อ Oracle Database (บทที่ 3: Act — ลงมือยิง POST Request)
เขียน Feature Test ใน Laravel สำหรับเชื่อมต่อ Oracle Database
หลังจากที่เราจัดเตรียมข้อมูล (Arrange) ตัวละครอย่าง User และ Payload ฟอร์มเรียบร้อยแล้ว ในบทนี้เราจะเข้าสู่ขั้นตอน Act หรือการ "ลงมือกระทำ" โดยจำลองการกด Submit ฟอร์มเพื่อส่งข้อมูลเข้าระบบจริงกันครับ
เป้าหมายของบทนี้: ส่ง Payload เข้าไปใน Flow การทำงานจริงของระบบ แล้วเก็บผลลัพธ์ (Response) ไว้ เพื่อเตรียมนำไปตรวจเช็ค (Assert) ในบทถัดไป
1. คำสั่งหลักประจำบท (The Core Command)
นำคำสั่งนี้ไปวางต่อจากส่วน Arrange ใน Test Method ของคุณ:
// ----- [ Act ] -----
$response = $this->post('/stock/store', $payload);
แยกส่วนประกอบคำสั่ง:
// ----- [ Act ] -----
$response = $this->post('/stock/store', $payload);
แยกส่วนประกอบคำสั่ง:
- $this->post(...) : คือการสั่งให้ Feature Test จำลองพฤติกรรมของ Browser ในการยิง HTTP POST Request
- '/stock/store' : คือ URL Path ปลายทาง (อิงตามไฟล์ routes/rmqc.php) ข้อสังเกต: เราใส่ Path ตรงๆ เพราะ Route นี้อาจจะไม่ได้ตั้งชื่อ (ไม่มี ->name()) ไว้
- $payload : ตัวแปร Array ข้อมูลฟอร์มที่เราเตรียมไว้จากบทที่ 2
- $response : ตัวแปรสำหรับเก็บ "ผลลัพธ์ทั้งก้อน" ที่ระบบตอบกลับมา (เช่น Status Code, การ Redirect, ข้อมูลใน Session) เพื่อนำไปใช้ต่อ
2. เกิดอะไรขึ้น "หลังบ้าน" เมื่อรันคำสั่งนี้? (Laravel Lifecycle)
สิ่งสำคัญที่ต้องรู้คือ การทำ Act ไม่ใช่การที่เราไปเรียก new SampleController() ด้วยตัวเอง แต่ HTTP Helper ของ Laravel จะจำลองการวิ่งผ่านระบบเต็มรูปแบบตามลำดับดังนี้:
- 📍 หา Route ปลายทาง: ระบบวิ่งไปที่ SampleController@store
- 🛡️ ผ่านด่าน FormRequest (StoreStockRequest):
- ถ้า Rules ผ่าน: ได้ไปต่อที่ Controller
- ถ้า Rules ไม่ผ่าน: ระบบจะเตะ Redirect กลับทันทีพร้อมแนบ Errors (ยังไม่ทันเข้าฟังก์ชัน store)
- ถ้า Rules ผ่าน: ได้ไปต่อที่ Controller
- ⚙️ ประมวลผลใน store():
- ดึง Auth::user()->id (จำได้ไหม? นี่คือผลลัพธ์จาก actingAs ในบทที่ 2)
- แปลง Request เป็น Data Transfer Object ($request->toDto())
- ส่งเข้า Action (createAction->execute()) เพื่อต่อ Oracle และ Insert ข้อมูลลงตาราง rmqc_inventory
- ดึง Auth::user()->id (จำได้ไหม? นี่คือผลลัพธ์จาก actingAs ในบทที่ 2)
- 🔄 ส่งผลลัพธ์กลับ: Controller สั่ง return redirect()->to(url('stock/edit', $id))->with('message', '...')
- 📦 เก็บลงกล่อง $response: นำทุกอย่างแพ็กรวมกันให้เราพร้อมนำไป Assert
3. อย่าลืมถอดเบรก! (ลบ markTestIncomplete)
หากในโค้ดเดิมคุณมีบรรทัด $this->markTestIncomplete('...'); ดักอยู่ ให้ลบออกได้เลย เพราะถ้าคุณวางคำสั่งนี้ไว้ ก่อน ขั้นตอน Act เทสจะหยุดทำงานและไม่ยอมยิง POST ให้คุณ
(คำแนะนำ: ระหว่างเรียนและเขียนเทส แนะนำให้ลบ Incomplete ออกไปเลยแล้วลุยเขียน Assert ในบทที่ 4 ต่อทันที)
4. โค้ดรวมหน้าตา Test Method หลังจบบทที่ 3
public function test_store_creates_inventory_and_redirects_to_edit(): void
{
// ----- [ Arrange ] (จากบทที่ 2) -----
$user = User::query()->first();
if (!$user) {
$this->markTestSkipped('ไม่มี user ใน rmqc_ap_users');
}
$this->actingAs($user);
$scheme = Scheme::query()->where('status', 1)->first();
if (!$scheme) {
$this->markTestSkipped('ไม่มี scheme status=1');
}
$payload = [
'scheme_id' => $scheme->id,
'fiscal_year' => 2569,
'lot' => 1,
'exam_no' => 1,
'volumn' => '100 ml',
'qty' => 10,
'expire_date' => '2027-12-31',
'status' => 1,
];
$expectedCode = "{$scheme->code}-{$payload['fiscal_year']}-{$payload['lot']}";
// ----- [ Act ] (จากบทที่ 3) -----
$response = $this->post('/stock/store', $payload);
// 💡 ทริคสำหรับการ Debug (เปิดใช้ชั่วคราวเพื่อดูข้อมูล แล้วลบออกก่อน Commit):
// $response->dump();
// $response->dumpSession();
// $this->withoutExceptionHandling();
// ----- [ Assert ] (รอติดตามในบทที่ 4) -----
}
5. คู่มือแก้ปัญหาฉบับเร่งด่วน (Troubleshooting)
public function test_store_creates_inventory_and_redirects_to_edit(): void
{
// ----- [ Arrange ] (จากบทที่ 2) -----
$user = User::query()->first();
if (!$user) {
$this->markTestSkipped('ไม่มี user ใน rmqc_ap_users');
}
$this->actingAs($user);
$scheme = Scheme::query()->where('status', 1)->first();
if (!$scheme) {
$this->markTestSkipped('ไม่มี scheme status=1');
}
$payload = [
'scheme_id' => $scheme->id,
'fiscal_year' => 2569,
'lot' => 1,
'exam_no' => 1,
'volumn' => '100 ml',
'qty' => 10,
'expire_date' => '2027-12-31',
'status' => 1,
];
$expectedCode = "{$scheme->code}-{$payload['fiscal_year']}-{$payload['lot']}";
// ----- [ Act ] (จากบทที่ 3) -----
$response = $this->post('/stock/store', $payload);
// 💡 ทริคสำหรับการ Debug (เปิดใช้ชั่วคราวเพื่อดูข้อมูล แล้วลบออกก่อน Commit):
// $response->dump();
// $response->dumpSession();
// $this->withoutExceptionHandling();
// ----- [ Assert ] (รอติดตามในบทที่ 4) -----
}
5. คู่มือแก้ปัญหาฉบับเร่งด่วน (Troubleshooting)
ถ้ารัน vendor/bin/phpunit --filter=SampleControllerStoreTest แล้วตัวหนังสือแดงแจ๋น อย่าเพิ่งตกใจ ลองเทียบอาการตามตารางนี้:
อาการที่พบ (Status Code) | สาเหตุที่พบบ่อย | แนวทางแก้ไข & วิธีตรวจสอบ
404 Not Found | Path ผิด หรือ Route โหลดไม่ขึ้น | เช็คว่าพิมพ์ '/stock/store' ถูกต้อง และมีอยู่ใน routes
302 Redirect ไปหน้า Login | Middleware ยังบล็อกอยู่ | กลับไปเช็คฟังก์ชัน setUp() ว่ามี $this->withoutMiddleware() หรือยัง
302 Redirect กลับ + มี Errors | ส่ง Payload ไม่ผ่าน Validation | ใส่ $response->dumpSession(); เพื่อดูว่าฟิลด์ไหนผิด Rule
500 Internal Server Error | โค้ดพัง หรือ Database มีปัญหา | ใส่ $this->withoutExceptionHandling(); ก่อนหน้า Act เพื่อให้โชว์ Error จริงๆ ออกมา
เทสค้าง ช้ามากจน Timeout | เชื่อมต่อ Oracle ไม่ได้ | เช็คการเชื่อมต่อ VPN หรือค่า Host ในไฟล์ .env
อาการที่พบ (Status Code) | สาเหตุที่พบบ่อย | แนวทางแก้ไข & วิธีตรวจสอบ
404 Not Found | Path ผิด หรือ Route โหลดไม่ขึ้น | เช็คว่าพิมพ์ '/stock/store' ถูกต้อง และมีอยู่ใน routes
302 Redirect ไปหน้า Login | Middleware ยังบล็อกอยู่ | กลับไปเช็คฟังก์ชัน setUp() ว่ามี $this->withoutMiddleware() หรือยัง
302 Redirect กลับ + มี Errors | ส่ง Payload ไม่ผ่าน Validation | ใส่ $response->dumpSession(); เพื่อดูว่าฟิลด์ไหนผิด Rule
500 Internal Server Error | โค้ดพัง หรือ Database มีปัญหา | ใส่ $this->withoutExceptionHandling(); ก่อนหน้า Act เพื่อให้โชว์ Error จริงๆ ออกมา
เทสค้าง ช้ามากจน Timeout | เชื่อมต่อ Oracle ไม่ได้ | เช็คการเชื่อมต่อ VPN หรือค่า Host ในไฟล์ .env
laravel