เขียน Feature Test ใน Laravel สำหรับเชื่อมต่อ Oracle Database (บทที่ 2: Arrange — เตรียมข้อมูล User, Scheme และ Payload)
เขียน Feature Test ใน Laravel สำหรับเชื่อมต่อ Oracle Database
หลังจากที่บทแรกเราได้ Set up ไฟล์และเชื่อมต่อฐานข้อมูล Oracle ไปแล้ว บทนี้เราจะมาเริ่มลงมือเขียนโค้ดใน Test Method กัน โดยเป้าหมายหลักคือการ เตรียมตัวแสดงและข้อมูลให้พร้อม ก่อนจะจำลองการยิง Request จริง (ยังไม่มีการ POST หรือตรวจเช็คผลลัพธ์ในบทนี้)
เป้าหมายของบทนี้: สร้างตัวแปร User เพื่อใช้ยืนยันตัวตน, ดึงข้อมูล Scheme ที่ใช้งานได้จริง และเตรียม Payload ข้อมูลฟอร์มให้ถูกต้องตามกฎของ Validation
1. รู้จักโครงสร้าง AAA (Arrange - Act - Assert)
การเขียน Test ที่ดีมักจะแบ่งโค้ดออกเป็น 3 ส่วนหลัก (AAA) เพื่อให้อ่านและทำความเข้าใจได้ง่าย:
1. รู้จักโครงสร้าง AAA (Arrange - Act - Assert)
การเขียน Test ที่ดีมักจะแบ่งโค้ดออกเป็น 3 ส่วนหลัก (AAA) เพื่อให้อ่านและทำความเข้าใจได้ง่าย:
- Arrange (บทนี้): จัดเตรียมสภาพแวดล้อม ข้อมูล และตัวแปรต่างๆ ให้พร้อม
- Act (บทที่ 3): ลงมือกระทำ เช่น การยิง HTTP POST Request ไปที่ Controller
- Assert (บทที่ 4): ตรวจสอบผลลัพธ์ว่า Database มีการเปลี่ยนแปลง หรือเกิดการ Redirect ตามที่คาดหวังหรือไม่
2. ดึงข้อมูล User จริงจาก Oracle Database
ฟังก์ชัน Store ของเราต้องการค่า Auth::user()->id เพื่อนำไปบันทึกเป็นคนสร้าง (created_by) และคนแก้ไข (updated_by) เราจึงต้องจำลองการ Login ของ User ก่อน:
$user = User::query()->first();
if (!$user) {
$this->markTestSkipped('ไม่มี user ในระบบ (rmqc_ap_users) — ข้ามเทสนี้');
}
$this->actingAs($user);
ทำความเข้าใจคำสั่ง:
คำสั่ง | เหตุผลที่ใช้
User::query()->first() | ใช้ User ที่มีอยู่จริงใน Oracle แทนการสร้าง Mock (ข้อมูลปลอม)
markTestSkipped() | หาก Database ว่าง ไม่ได้แปลว่าโค้ดพัง จึงให้ข้าม (Skip) เทสไปเลย ไม่ต้องขึ้น Fail
actingAs($user) | จำลองให้ Request นี้ทำงานเสมือนว่า User คนนี้ได้ทำการ Login เข้ามาแล้ว
(หมายเหตุ: แม้บทที่ 1 เราจะปิด Middleware ไปแล้ว แต่คำสั่ง actingAs ยังจำเป็นมาก เพราะ Controller มีการเรียกใช้งาน Auth::user() โดยตรง)
ฟังก์ชัน Store ของเราต้องการค่า Auth::user()->id เพื่อนำไปบันทึกเป็นคนสร้าง (created_by) และคนแก้ไข (updated_by) เราจึงต้องจำลองการ Login ของ User ก่อน:
$user = User::query()->first();
if (!$user) {
$this->markTestSkipped('ไม่มี user ในระบบ (rmqc_ap_users) — ข้ามเทสนี้');
}
$this->actingAs($user);
ทำความเข้าใจคำสั่ง:
คำสั่ง | เหตุผลที่ใช้
User::query()->first() | ใช้ User ที่มีอยู่จริงใน Oracle แทนการสร้าง Mock (ข้อมูลปลอม)
markTestSkipped() | หาก Database ว่าง ไม่ได้แปลว่าโค้ดพัง จึงให้ข้าม (Skip) เทสไปเลย ไม่ต้องขึ้น Fail
actingAs($user) | จำลองให้ Request นี้ทำงานเสมือนว่า User คนนี้ได้ทำการ Login เข้ามาแล้ว
(หมายเหตุ: แม้บทที่ 1 เราจะปิด Middleware ไปแล้ว แต่คำสั่ง actingAs ยังจำเป็นมาก เพราะ Controller มีการเรียกใช้งาน Auth::user() โดยตรง)
3. ดึงข้อมูล Scheme ที่พร้อมใช้งาน (Active)
เนื่องจากใน CreateStockAction มีการเรียกหา Scheme และมีเงื่อนไข Validation ว่า scheme_id ต้องมีอยู่ในระบบ (exists:rmqc_scheme,id) เราจึงต้องดึง Scheme ของจริงมาใช้ทดสอบ:
$scheme = Scheme::query()->where('status', 1)->first();
if (!$scheme) {
$this->markTestSkipped('ไม่มี scheme ที่มี status=1 — ข้ามเทสนี้');
}
ทำไมต้อง status = 1? เพื่อให้การจำลองสถานการณ์ใกล้เคียงความจริงมากที่สุด เพราะในหน้าจอ Create ของระบบจริงก็จะกรองเฉพาะ Scheme ที่พร้อมใช้งานเท่านั้น
เนื่องจากใน CreateStockAction มีการเรียกหา Scheme และมีเงื่อนไข Validation ว่า scheme_id ต้องมีอยู่ในระบบ (exists:rmqc_scheme,id) เราจึงต้องดึง Scheme ของจริงมาใช้ทดสอบ:
$scheme = Scheme::query()->where('status', 1)->first();
if (!$scheme) {
$this->markTestSkipped('ไม่มี scheme ที่มี status=1 — ข้ามเทสนี้');
}
ทำไมต้อง status = 1? เพื่อให้การจำลองสถานการณ์ใกล้เคียงความจริงมากที่สุด เพราะในหน้าจอ Create ของระบบจริงก็จะกรองเฉพาะ Scheme ที่พร้อมใช้งานเท่านั้น
เช็คลิสต์ในใจก่อนไปต่อ:
- $scheme->id ต้องมีค่าเพื่อใช้ส่งเป็น Payload
- $scheme->code ต้องมีค่า เพราะต้องใช้ประกอบสร้างเลข Code ในขั้นตอน Assert (บทที่ 4)
4. จำลอง Payload ให้ผ่าน Validation (StoreStockRequest)
เตรียมข้อมูลจำลองที่จะส่งไปกับ Request โดยอิงตามกฎ (Rules) ของ Form Request ดังนี้:
$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, // ค่าเทียบเท่า StockStatusEnum::ACTIVE
// เราจงใจไม่ส่งค่า 'picture' ในรอบนี้
];
เคล็ดลับการสร้าง Payload:
เตรียมข้อมูลจำลองที่จะส่งไปกับ Request โดยอิงตามกฎ (Rules) ของ Form Request ดังนี้:
$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, // ค่าเทียบเท่า StockStatusEnum::ACTIVE
// เราจงใจไม่ส่งค่า 'picture' ในรอบนี้
];
เคล็ดลับการสร้าง Payload:
- Key ของ Array: ต้องใช้ชื่อที่ตรงกับช่องกรอกข้อมูล (Input form) เช่น ใช้ scheme_id ไม่ใช่ schemeId (ซึ่งเป็นชื่อในฝั่ง DTO)
- ข้อมูลเฉพาะ: หากส่งค่า lot หรือ exam_no นอกเหนือจากเลข 1, 2, 3 เทสจะตก (Fail) ตั้งแต่ด่าน Validation ทันที และจะไปไม่ถึง Controller
- รูปภาพ: ยังไม่จำเป็นต้องแนบไฟล์รูปภาพในขั้นตอนนี้ เพื่อลดความซับซ้อนเรื่อง Storage และโฟกัสไปที่ Flow การบันทึกข้อมูลหลักก่อน
5. คำนวณค่าล่วงหน้า (เตรียมไว้สำหรับตอน Assert)
ใน CreateStockAction จะมีการสร้างหมายเลข Inventory ขึ้นมาใหม่จากสูตร: code-fiscalYear-lot เพื่อไม่ให้เราต้องไปเดาข้อมูลในตอนตรวจสอบผลลัพธ์ เราจะคำนวณเก็บไว้เลยตั้งแต่ขั้นตอนนี้:
$expectedCode = "{$scheme->code}-{$payload['fiscal_year']}-{$payload['lot']}";
// ผลลัพธ์ตัวอย่าง: "CHM-2569-1"
6. รวมโค้ด Arrange ฉบับสมบูรณ์ (นำไปใส่ใน Test Method)
เมื่อนำทุกส่วนประกอบมารวมกัน โค้ดสำหรับฟังก์ชันเทสในบทนี้จะออกมาเป็นรูปแบบนี้:
public function test_store_creates_inventory_and_redirects_to_edit(): void
{
// ----- [ Arrange ] -----
// 1. เตรียม User ยืนยันตัวตน
$user = User::query()->first();
if (!$user) {
$this->markTestSkipped('ไม่มี user ใน rmqc_ap_users');
}
$this->actingAs($user);
// 2. เตรียม Scheme ที่ Active
$scheme = Scheme::query()->where('status', 1)->first();
if (!$scheme) {
$this->markTestSkipped('ไม่มี scheme status=1');
}
// 3. จำลองข้อมูลจากฟอร์ม
$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,
];
// 4. คำนวณค่าที่จะใช้เช็คผลลัพธ์
$expectedCode = "{$scheme->code}-{$payload['fiscal_year']}-{$payload['lot']}";
// ----- [ Act ] (บทที่ 3) -----
// $response = $this->post('/stock/store', $payload);
// ----- [ Assert ] (บทที่ 4) -----
// ...
$this->markTestIncomplete('จบ Arrange แล้ว — เตรียมไปต่อกันที่บท 3: Act');
}
Checklist ประจำบทที่ 2
ก่อนไปต่อที่บทที่ 3 ตรวจสอบความพร้อมของเทสคุณอีกครั้ง:
ใน CreateStockAction จะมีการสร้างหมายเลข Inventory ขึ้นมาใหม่จากสูตร: code-fiscalYear-lot เพื่อไม่ให้เราต้องไปเดาข้อมูลในตอนตรวจสอบผลลัพธ์ เราจะคำนวณเก็บไว้เลยตั้งแต่ขั้นตอนนี้:
$expectedCode = "{$scheme->code}-{$payload['fiscal_year']}-{$payload['lot']}";
// ผลลัพธ์ตัวอย่าง: "CHM-2569-1"
6. รวมโค้ด Arrange ฉบับสมบูรณ์ (นำไปใส่ใน Test Method)
เมื่อนำทุกส่วนประกอบมารวมกัน โค้ดสำหรับฟังก์ชันเทสในบทนี้จะออกมาเป็นรูปแบบนี้:
public function test_store_creates_inventory_and_redirects_to_edit(): void
{
// ----- [ Arrange ] -----
// 1. เตรียม User ยืนยันตัวตน
$user = User::query()->first();
if (!$user) {
$this->markTestSkipped('ไม่มี user ใน rmqc_ap_users');
}
$this->actingAs($user);
// 2. เตรียม Scheme ที่ Active
$scheme = Scheme::query()->where('status', 1)->first();
if (!$scheme) {
$this->markTestSkipped('ไม่มี scheme status=1');
}
// 3. จำลองข้อมูลจากฟอร์ม
$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,
];
// 4. คำนวณค่าที่จะใช้เช็คผลลัพธ์
$expectedCode = "{$scheme->code}-{$payload['fiscal_year']}-{$payload['lot']}";
// ----- [ Act ] (บทที่ 3) -----
// $response = $this->post('/stock/store', $payload);
// ----- [ Assert ] (บทที่ 4) -----
// ...
$this->markTestIncomplete('จบ Arrange แล้ว — เตรียมไปต่อกันที่บท 3: Act');
}
Checklist ประจำบทที่ 2
ก่อนไปต่อที่บทที่ 3 ตรวจสอบความพร้อมของเทสคุณอีกครั้ง:
- [ ] มีการเช็คและใช้งาน $user คู่กับ actingAs() เรียบร้อยแล้ว
- [ ] ดึง $scheme ที่มีสถานะพร้อมใช้งานสำเร็จ
- [ ] ข้อมูลใน $payload มีฟิลด์ครบถ้วนและถูกต้องตาม Rules ของระบบ
- [ ] เตรียมตัวแปร $expectedCode ไว้รอแล้ว
- [ ] โค้ดยังไม่มีคำสั่งรัน POST Request หรือตรวจเช็คฐานข้อมูล (เก็บไว้ทำในบทถัดไป)
laravel