บทความนี้จะเป็นการฝึกเขียน Test PHP ด้วย FizzBuzz เกมง่ายๆ โดยหลายๆคนคงจะได้ผ่านบททดสอบนี้ คล้ายๆกับการเริ่มเขียนโปรแกรมต้องเขียน Hello world นั่นแหละยังไง ยังงั้นเลย โดยผมอาจจะไม่ได้อัพขึ้น git hub ทีละขั้นนะครับอาจจะเป็นแบบไฟล์สำเร็จเลยแล้วอ่านเอาจากบทความนี้นะครับ ถ้าใครมีคำถามหรือข้อเสนอก็รบกวนเขียนฝากไว้ด้านล่างนะครับ
เริ่มดูโจทย์แล้วคิด
หลายๆคนก็น่าจะเป็นเหมือนผมได้โจทย์มาอ่านเสร็จเริ่มเขียนเลย อย่างแรกเราไปดูโจทย์หรือ Requirement กันก่อนแหละเพื่อให้เราเข้าใจว่าโจทย์ต้องการอะไร แล้วเราจะเริ่มเขียน Test อะไรก่อน ขอออกตัวก่อนนะครับว่าผมไม่ได้เชี่ยวชาญนะครับ เพราะฉะนั้นอาจจะเริ่มคิดไม่เหมือนคนอื่น อย่างที่บอกไปในตอนแรกถ้าคิดต่างขอแชร์กันได้นะครับ ผมจะได้เปิดโลกมากขึ้น เอาต่อ โจทย์เป็นดังนี้
โจทย์
เกม Fizzbuzz จะให้เขียน function หรือ class ก็ได้ โดยเมื่อรันแล้วจะทำการ echo/print ตัวเลข โดยตัวเลขตำ่สุดคือ 1 จนถึงจำนวนที่ใส่โดยถ้าไม่ส่งค่าให้ตัวเลขสุดท้ายอยู่ที่ 15 และหากตัวเลขใดหารด้วย 3 ลงตัวให้ echo/print คำว่า ‘fizz’ และหากตัวเลขใดหารด้วย 5 ลงตัวให้ echo/print คำว่า ‘buzz’ แทนตัวเลขนั้นๆ หากลงตัวทั้งคู่ให้ echo/print คำว่า ‘fizzbuzz’ อย่างนี้ตัวอย่างหากเราเรียกแล้วผลลัพธ์จะทำนองนี้
‘1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz’
จากโจทย์แค่นี้เราจะเขียน test ได้เป็นหมื่นเคสเลย ( เวอร์ไป ) เอาจริงๆก็คือเยอะอยู่นะ ถ้าเราละเอียด แต่อย่างที่เคยบอกไปในบทความฝึกเขียนครั้งแรกว่า จะเขียนจำนวนเคสเยอะหรือน้อยขึ้นอยู่กับเวลาและความรุนแรงของ function นั้นๆ หากมันเป็นเกี่ยวข้องกับหลายส่วนหรือสำคัญก็ควรจะคิดให้เยอะๆหลายๆด้านหน่อยครับโอเคเรามาคิดเรื่องเคสที่เกิดขึ้นได้ก่อนจากโจทย์มีดังนี้
- ค่าเริ่มแรกเป็น 1
- ค่าสุดท้ายเท่าไรก็ได้แต่ถ้าไม่ส่งก็ 15
- หาร 3 ลงตัว echo fizz
- หาร 5 ลงตัว echo buzz
- หากลงตัวทั้ง 3 และ 5 echo fizzbuzz
ก่อนจะเริ่มเขียน Test แรกแนะนำให้ไป set up Test ก่อนนะครับใครยังทำไม่เป็นก็ไปดูที่นี่ครับ [PHP] ฝึกเขียน Test กันเถอะ LV Beginner
Test แรก fizzbuzz
เริ่มต้นก็อยู่ที่เราครับว่าจะเทสอะไร คาดหวังอะไรตรงไหน ค่อยๆคิดครับไม่ต้องรีบ อย่างผมตอนแรกอยากจะลองเริ่มเทสว่าเรามี function execute ( แล้วแต่จะตั้งชื่อนะครับ ) แบบเรียกใช้โดยลองส่ง 3 เข้าไปแล้วจะได้คำว่า fizz กลับมาเราจะเขียน เทส ประมาณนี้ครับ
require_once dirname(__FILE__) . '/../Fizzbuzz.php'; class FizzbuzzTest extends PHPUnit_Framework_TestCase { function testResultShouldBeFizz() { $result = $this->fizzbuzz->execute(3); $this->assertEquals($result,'fizz'); } }
เมื่อลองรัน Test ดูจะพบว่าเรายังไม่ได้สร้าง class Fizzbuzz ด้วยซ้ำนะครับ เพราะฉะนั้นตัว Test นี้จะช่วยให้เราทำตามขั้นตอนได้อย่างดีครับ โดยเราจะเริ่มสร้าง class Fizzbuzz มาครับ แล้วก็สร้าง function execute ต่อเลย เสร็จเราอยากให้เทสผ่านก็ทำการ return ‘fizz’ ได้เลยครับเทสแรกเราก็จะผ่านแหละ
class Fizzbuzz { public function execute($number) { return 'fizz'; } }
ต่อมาเราจะเทสว่าถ้าได้รับเลข 5 จะคืนค่าเป็น ‘buzz’ กลับมาโดยเราจะเพิ่ม Test ต่ออีกหนึ่ง function ครับเป็นดังนี้
function testResultShouldBeBuzz() { $result = $this->fizzbuzz->execute(5); $this->assertEquals($result,'buzz'); }
ซึ่ง ณ ตอนนี้ Test จะช่วยเหลือเราแล้วว่าจะใส่ 3 ใส่ 5 ต่อแล้วผลออกมาอย่างที่เราต้องการไหม ? เมื่อเทสก็จะไม่ผ่านติดตัวแดง ผมอาจจะไม่ได้ทำภาพให้ดูนะครับไปดูบทแรกที่สอนนะครับ บทนี้จะมาทำให้ดูว่าคิดยังไง ทำยังไงครับ ต่อมาเราต้องไปแก้ตัวไฟล์ Fizzbuzz.php ให้เทสผ่านนะครับโดยจะเป็นอย่างนี้ครับ
public function execute($number) { if( ($number == 3) return 'fizz'; if( ($number == 5) return 'buzz'; }
ก็จะเทสผ่านนะครับ แต่สังเกตุไหมครับว่าถ้าส่งตัวเลขอื่นเข้าไปจะเป็นยังไง นั่นแหละครับต่อไปเราก็ลองส่งตัวเลขที่ไม่ใช่ 3 หรือ 5 ดูครับ ว่าจะเป็นอย่างไร กลับไปแก้ไข FizzbuzzTest.php ครับ คราวนี้เราจะทดสอบเรื่องที่ว่าส่งค่าเป็น 1 ก็ต้องกลับมาเป็น 1 หรือ 2 ก็ต้องส่งกลับเป็น 2 ครับลองทีละอันครับ
function testResultShouldBeOne() { $result = $this->fizzbuzz->execute(1); $this->assertEquals($result,'1'); }
เราก็จะไปแก้ไขง่ายด้วยว่าเพิ่ม return $number; จะได้แล้วครับดังนี้
public function execute($number) { if( ($number == 3) return 'fizz'; if( ($number == 5) return 'buzz'; return $number; }
ถ้าแก้ไขครั้งนี้เราจะรองรับ 1 , 2 , 4 ได้หมดแต่ … คำถามคือเรามั่นใจได้อย่างไร ? แน่นอนครับเราทำ Test อยู่ก็ทำ Test ในสิ่งที่เราไม่มั่นใจไงครับ งั้นเราก็ลองดูว่าส่ง 2 , 4 เข้าไปได้กลับมาเป็นอย่างที่เราทำไหม
function testResultShouldBeTwo() { $result = $this->fizzbuzz->execute(2); $this->assertEquals($result,'2'); }
function testResultShouldBeFour() { $result = $this->fizzbuzz->execute(4); $this->assertEquals($result,'4'); }
ลองดูว่าผ่านไหมนะครับ เสร็จอย่างที่เราดูว่าโค้ดที่เราเขียนนั้นยังไม่รองรับเลข 6 ซึ่งหาร 3 ลงตัวมันควรจะเป็นคำว่า ‘fizz’ ออกมา ถ้าเราไม่มั่นใจก็ลองครับอย่างที่บอก
function testPutSixResultShouldBeFizz() { $result = $this->fizzbuzz->execute(6); $this->assertEquals($result,'fizz'); }
สังเกตุนะครับชื่อ test เราควรจะเขียนให้เข้าใจด้วยเวลามันขึ้นตัวแดงเราจะได้อ่านแล้วรู้ว่ามันผิดตรงไหนอย่างไร เมื่อรันเทสเราจะไม่ผ่าน เพราะโค้ดที่เราเขียนในตอนแรกนั้นมันเช็คว่าต้องเท่ากับ 3 นะครับไม่ใช่หาร 3 ลงตัวเราก็ต้องไปแก้ไข function ดูครับเป็น
public function execute($number) { if( ($number % 3) == 0 ) return 'fizz'; if( ($number % 5) == 0 ) return 'buzz'; return $number; }
จริงๆคือการเช็คค่าของ 5 ก็เหมือนกันครับผมถือโอกาสแก้ไขเลย แล้วลองรันเทสครับว่าผ่านหมดไหม ถ้าผ่านหมดนั้นแปลว่าการแก้ไขโค้ดของเรา ไม่ได้ทำให้ของเก่าพังเลยนั่นแหละครับเห็นประโยชน์มันหรือยังล่ะ ไม่ต้องมาใส่ค่าซ้ำๆอย่างที่เคยทำครับ
ตอนนี้เราสำเร็จไปหลายเคสแหละเราลองมาทบทวนดูครับว่าตอนนี้เราเทสอะไรผ่านไปแล้วแล้วเหลืออะไรบ้าง
- ค่าเริ่มแรกเป็น 1 ( ยังไม่ได้ทำ )
- ค่าสุดท้ายเท่าไรก็ได้แต่ถ้าไม่ส่งก็ 15 ( ยังไม่ได้ทำ )
- หาร 3 ลงตัว echo fizz ( ทำแล้ว )
- หาร 5 ลงตัว echo buzz ( ทำแล้ว )
- หากลงตัวทั้ง 3 และ 5 echo fizzbuzz ( ยังไม่ได้ทำ )
โอเคงั้นต่อไปเราเทสเรื่องถ้าหากเป็นตัวเลข 15 คือหารลงด้วย 3 และ 5 จะคือค่ากลับมาเป็น fizzbuzz ดูครับดังนี้
function testResultShouldBeFizzBuzz() { $result = $this->fizzbuzz->execute(15); $this->assertEquals($result,'fizzbuzz'); }
เสร็จแล้วเราก็กลับไปแก้ไขไฟล์ Fizzbuzz.php เป็น
public function execute($number) { if( ($number % 3) == 0 && ($number % 5) == 0 ) return 'fizzbuzz'; if( ($number % 3) == 0 ) return 'fizz'; if( ($number % 5) == 0 ) return 'buzz'; return $number; }
โอเคเสร็จแล้วคราวนี้เกมมันนี้เราต้องมีอีก 1 function สำหรับการทำการวนเลขให้นั่นแหละ เพราะฉะนั้นเราจะทดสอบการวนเลขก่อนครับโดยเทสหน้าตาจะเป็นแบบนี้ครับ
function testResultShouldBeNumberOneToFifteen() { $result = $this->fizzbuzz->play(); $this->assertEquals($result,'1 2 3 4 5 6 7 8 9 10 11 12 13 14 15'); }
โดยเมื่อรัน test ระบบจะแจ้งว่าเรายังไม่มี function play ก็แน่นอนแหละ ก็ไปสร้าง function play ครับแล้วก็ return ง่ายๆมาเลยแบบที่เทสมันต้องการดังนี้
function play () { return '1 2 3 4 5 6 7 8 9 10 11 12 13 14 15'; }
แต่ๆ เกมนี้มันไม่ได้จำนวนกัดว่าจะใส่ให้มันวนเลขเท่าไร เช่น ถ้าเราใส่ play(20) มันก็ควรจะวน 1 – 20 ถูกต้องไหมครับ เพราะฉะนั้นเราก็ต้องลองครับเขียนเคสเลย
function testResultShouldBeNumberOneToTwenty() { $result = $this->fizzbuzz->play(); $this->assertEquals($result,'1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20'); }
คราวนี้เราต้องแก้ไขให้ function เราวนเลขแหละโดยถ้าไม่ใส่มาเราจะกำหนดให้เป็น 15 เหมือนที่โจทย์ตั้งไว้ครับเป็นดังนี้
public function play($endNumber = 15) { $startNumber = 1; $result = ''; for( $number = $startNumber; $number <= $endNumber; $number ++ ) { $result .= $number . ' '; } return substr($result, 0, -1); }
คราวนี้เราจะทำการทดสอบ 2 เคสนั้นได้แล้วคือ ค่าเริ่มต้นถ้าไม่ส่งมาให้เป็น 1 และค่าสุดท้ายเป็น 15 แต่ถ้าส่งมาก็จะนับเลขตามที่เราใส่ ลองรันเทสดูครับ สุดท้ายเราจะสามารถ refactor test ก็ได้นะครับเมื่อตอนแรกเราให้ค่าเป็น 1 – 15 แต่คราวนี้เราต้องการให้ทำการวนเลขและทำการเช็คค่าด้วยว่าเป็น fizz หรือ buzz เราสามารถแก้ไข Test เป็นดังนี้
function testResultShouldBeNumberOneToFifteenWithFizzBuzz() { $result = $this->fizzbuzz->play(); $this->assertEquals($result,'1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz'); }
และสุดท้ายเราก็รันเทสแล้วแก้ไขไฟล์ที่เราเทส ดังนี้ครับ
public function play($endNumber = 15) { $startNumber = 1; $result = ''; for( $number = $startNumber; $number <= $endNumber; $number ++ ) { $result .= $this->execute($number) . ' '; } return substr($result, 0, -1); }
ก็จะเสร็จแล้วครับสำหรับบทนี้ ใครอยากดูไปดูที่ github ได้ครับ https://github.com/oxygenyoyo/fizzbuzz_php
อ้าวจบแล้ว ?
เอาจริงๆผมเชื่อว่าหลายๆคนก็จะมีไอเดียแล้วว่าจะเทสอะไรต่อเช่น ทำไมไม่เขียนเทสสำหรับการส่งค่าเป็นค่าลบ หรือเป็น String สำหรับค่าตัวเลขสุดท้ายเช่น play(-1) หรือ play(‘test’) แล้วให้คืนค่าแบบที่ exception มาก็ได้ หรือให้แสดงข้อความแจ้งเตือนก็ได้ ซึ่งถ้าหากคุณอ่านถึงตรงนี้แล้วคิดว่ามีหลายเคสที่ผมไม่ได้ทำ แปลว่า คุณเริ่มเข้าใจแล้วว่าการเขียน เทสมันช่วยอะไรบ้าง
- ทำให้เรานึกถึงความเป็นไปได้ของโค้ดของเรา
- ทำให้เรารอบคอบ
- ทำให้เราไม่ต้องใส่ค่าเทสที่เคยทำไปแล้ว ทำซ้ำๆให้เรา
แต่การฝึกทำเทสเนี้ยไม่ได้หมายความว่าจะไม่มี Bug นะครับ มันทำให้เรารอบคอบขึ้น แต่พวกกรณีที่เราคิดไม่ถึงนั้นก็เป็น Bug นะครับอย่าลืม แต่ถามว่าเราจะเขียนโค้ดดีขึ้นไหมถ้าคุณนึกถึงหลายๆกรณีได้แปลว่าคุณเริ่มพัฒนาแล้วครับ
ถ้าหากมีอะไรที่ผมเขียนพลาดหรือมีอะไรที่ไม่เข้าใจลองมาแชร์กันนะครับ