ทบทวนเรียน Laravel จากการเขียน test
ช่วงนี้มีโอกาสได้สลับมาเขียนภาษา PHP บน Laravel Framework พบว่า ecosystem ของเขาไปไกลมากกว่าที่เราเคยรู้มาก่อนหน้านี้เยอะเลย เช่น การขึ้น project ใหม่ด้วย Composer โดยเฉพาะ Model-View-Controller ที่มี frontend เป็นรูปแบบ server-side rendering หรือ managed service เช่น payment, caching, deployment ก็มีมาให้ครบ
แต่เพื่อความรวดเร็วในการเรียนรู้สิ่งใหม่ ๆ เราเลยลองใช้ technique เดียวกับตอนที่เรียนภาษาอื่น ๆ คือ เรียนจากการแก้ test ให้มันผ่าน อย่างของภาษา Go เขาก็มี Learn Go with Tests ก็เลยลองแนวคิดนี้มาประยุกค์กับการเรียน PHP + Laravel ไปเลย
หมายเหตุ: เราประยุกต์โจทย์มาจาก blog [PHP] เรียนรู้การพัฒนา Web Application ด้วย Laravel framework ตามแนวคิด Test Driven อีกทีนะ
1. เริ่มต้นด้วยการขึ้น project ใหม่ก่อน
$ composer create-project laravel/laravel <your-project-name>
2. เริ่มเขียน test แรก
โดยโจทย์ของเราคือเราจะมี User
ที่มี Account
ได้หลาย ๆ อัน เมื่อมีข้อมูลทั้งสองอยู่ใน database ตอนที่ call API GET /users/<username>
จะต้องส่งข้อมูลกลับมา ก็จะได้ test หน้าตาประมาณนี้
พอลองเขียนแล้วลอง run test ปรากฎว่า run ได้ แต่เจอ error ประมาณนี้
Error: Class "Tests\Feature\User" not found
สิ่งที่ได้เรียนรู้เพิ่มเติม
- PHP เป็น interpreted language (ทำการแปลง code เป็น machine instruction ไปเรื่อย ๆ จนกว่าจะเจอ error) เพราะว่าเรายังไม่ได้สร้าง class
User
แต่ยัง run code ขึ้นมาได้ - การสร้าง record ใหม่จาก model เพื่อ insert ลง database ผ่าน Eloquent ซึ่งเป็น ORM ที่ Laravel เพิ่มเข้ามา โดยความต่างของ
create
กับmake
คือcreate
จะทำการ save ลง database ให้โดยอัตโนมัติ ในขณะที่make
จะต้องไปเรียก functionsave
ลง database อีกที - การใช้งาน assertion ต่าง ๆ ผ่าน PHPUnit ซึ่งเป็น testing framework ที่ Laravel เพิ่มเข้ามา
3. Import User model เข้ามา
ลอง run test ปรากฎว่า error แต่คราวนี้ message เปลี่ยนไป ถือว่ามีพัฒนาการ ฮ่า ๆๆ
Illuminate\Database\QueryException: SQLSTATE[08006] [7] connection to server at "127.0.0.1", port ... failed: Connection refused
4. เปิดการใช้งาน in-memory database สำหรับการทดสอบ
ตอนนี้เราไม่สามารถเชื่อมต่อ database เพื่อ save User
ลงไปได้ ซึ่งวิธีแก้ก็คือสร้าง database ขึ้นมาเพื่อทำการทดสอบโดยเฉพาะ ซึ่งตอนแรกเราว่าจะใช้ PostgreSQL ที่สร้างผ่าน Docker แต่นั่นก็หมายความว่าเมื่อต้องการจะทดสอบเราจะต้อง run PostgreSQL โดยเฉพาะถ้า run บน CI จะต้องออกแรงติดตั้งเพิ่มอีก
โชคดีที่ PHPUnit เขามี in-memory SQLite ให้เลยโดยไม่ต้องไปติดตั้ง database เองโดยไม่จำเป็น เราแค่ต้องไป uncomment configuration ใน phpunit.xml
ตรง DB_CONNECTION
กับ DB_DATABASE
ครับ
ลอง run test ปรากฎว่า error แจา message เปลี่ยนไปเช่นเคย
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such table: users
5. ทำการ migrate database schema ลงใน in-memory database
ใน test case ให้เราเพิ่ม use RefreshDatabase;
เข้าไปเพื่อทำการ migrate และ clean database ทั้งก่อนและหลังการ run test ซึ่งโดยปกติแล้ว Laravel จะเก็บ migration ไว้ที่ database/migrations
ซึ่งจะมีทั้ง up
และ down
สำหรับ forward และ reverse กลัับ
ลอง run test ปรากฎว่า error แต่ message เปลี่ยนไปเช่นเคย
Error: Class "Tests\Feature\Account" not found
6. สร้าง Account model ขึ้นมา
ให้เราสร้าง model ใหม่ด้วยคำสั่ง
$ php artisan make:model Account
Artisan ซึ่งเป็น command-line interface ของ Laravel จะไปสร้าง model Account
ใน directory app/Models
หน้าตาประมาณนี้
ลอง run test ปรากฎว่า error แต่ message เปลี่ยนไปเช่นเคย
Error: Class "Database\Factories\AccountFactory" not found
7. สร้าง Factory ขึ้นมา
Factory คือสิ่งที่ Eloquent เตรียมไว้ให้เพื่อทำการเพิ่ม record ลงไปใน database สำหรับใช้ในการทดสอบโดยเฉพาะ (seeding) เพื่อแยกส่วนของการเตรียมข้อมูลที่ใช้ในการทดสอบออกจาก test code ทำให้อ่านง่ายขึ้น นอกจากนั้นแล้วยังสามารถ generate attribute แบบสุ่มโดยใช้ Faker ได้ด้วย ทำให้ชุดการทดสอบเราแข็งแรงดัก edge case ได้ดีมากขึ้น ดังนั้นให้เราสร้าง factory ใหม่ด้วยคำสั่ง
$ php artisan make:factory AccountFactory
คำสั่งนี้จะไปสร้าง factory AccountFactory
ใน directory database/factories
จากนั้นตรง return statement ของ function definition
ให้ทำการ return array โดย Account
ของเราจะมี field ชื่อว่า account_no
ซึ่งให้ทำการสุ่มชื่อไว้เป็น default
ลอง run test ปรากฎว่า error แต่ message เปลี่ยนไปเช่นเคย
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such table: accounts
8. สร้าง migration file สำหรับ Account
ถ้าเราเข้าไปดู migration ที่ database/migrations
จะพบว่ามันมี migration file ติดตั้งไว้ให้พร้อมแล้วสำหรับ User
แต่ว่าของ Account
ยังไม่มี จึงต้องไปสร้างใหม่ด้วยคำสั่ง
$ php artisan make:migration create_accounts_table --create=accounts
พอเราเข้าไปที่ migration file ใหม่ใน functiom up
ให้ทำการกำหนด column ใหม่ 2 อันก็คือ user_id
เพื่อเชื่อม key ไปหา User
และ account_no
ของตัวมันเอง
ลอง run test ปรากฎว่า error แต่ message เปลี่ยนไปเช่นเคย
BadMethodCallException: Call to undefined method App\Models\User::accounts()
9. ประกาศ relationship ระหว่าง User และ Account
ใน test case ตรง statement $user->accounts()->save($newAccount);
หมายถึงว่าเราจะทำการเพิ่ม newAccount
เข้าไปเป็นหนึ่งใน accounts
ของ User
แล้วก็ save ลง database จาก error message คือเรายังไม่ได้ประกาศ relationship ว่า User
มีได้หลาย Account
ให้เราไปสร้าง function ใหม่ใน User
ดังนี้
ลอง run test ปรากฎว่า error แต่ message เปลี่ยนไปเช่นเคย
Expected response status code [200] but received 404.
10. เพิ่ม routing ของ URL path /users/{username}
ในกรณีนี้เราจะรับ username
เข้ามาเป็น path variable เพื่อที่จะนำไปหาใน database ให้ return ข้อมูล User ที่ name
ตรงกับ username
ให้ไปที่ routes/web.php
จากนั้นเพิ่ม route ใหม่ลงไป
ลอง run test ปรากฎว่า error แต่ message เปลี่ยนไปเช่นเคย
Expected response status code [200] but received 500.
11. เพิ่ม UserController แล้วเชื่อมกับ route ที่สร้างไว้ก่อนหน้า
สร้าง controller ใหม่ด้วยคำสั่ง
$ php artisan make:controller UserController
จะได้ file ใหม่อยู่ที่ directory app/Http/Controllers/
จากนั้นเข้าไปเพิ่ม function show
จากนั้นกลับไปที่ routes/web.php
แล้วก็เชื่อม UserController
เข้ากับ route ใหม่ประมาณนี้
ลอง run test ปรากฎว่าผ่านแล้ว!
12. Refactor
ใน UserController
เราสามารถหยิบ statement User::with('accounts')->where('name', $username)->first()
ให้เป็น function ข้างใน User
model ได้ เช่น findByName($username)
แทน ให้เราเริ่มต้นจากการสร้าง test ขึ้นมาอีกชุดนึงสำหรับทดสอบ function findByName
โดยเฉพาะ จะได้หน้าตาประมาณนี้
จากนั้นทำการ run test ก็จะพบว่า error
BadMethodCallException: Call to undefined method App\Models\User::findByName()
ให้เราไปเพิ่ม function ใหม่ใน User
model ประมาณนี้
ลอง run test ใหม่ปรากฎว่าผ่านแล้ว! คราวนี้เราก็สามารถ refactor UserController
ตามที่บอกไปได้
ลอง run test อันเก่าปรากฎว่ายังผ่านอยู่ เป็นอันจบงาน
สรุป
จากการนำ technique ในการเรียนรู้ภาษา PHP และ Laravel framework ผ่านการทดสอบทำให้เราเข้าใจจุดประสงค์ของส่วนประกอบแต่ละชื้นใน Laravel ได้ลึกซึ้งมากขึ้น เพราะเราจะเห็นเลยว่าถ้าขาดส่วนไหนไป อะไรจะเกิดขึ้น ไม่ว่าจะเป็น
- Eloquent ORM
- Model
- Factory
- Database migration
- Routing
- Controller
- Testing
- Laravel command line
ลองนำ technique นี้ไปใช้ในการเรียนรู้ภาษาใหม่ ๆ กันดูครับ