phpunit test traning

PHP ฝึกเขียน Test Fizzbuzz

บทความนี้จะเป็นการฝึกเขียน 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 นะครับอย่าลืม แต่ถามว่าเราจะเขียนโค้ดดีขึ้นไหมถ้าคุณนึกถึงหลายๆกรณีได้แปลว่าคุณเริ่มพัฒนาแล้วครับ

ถ้าหากมีอะไรที่ผมเขียนพลาดหรือมีอะไรที่ไม่เข้าใจลองมาแชร์กันนะครับ

Loading

เป็นโปรแกรมเมอร์ที่ตามหาคุณค่าของชีวิตและความฝันในวัยเด็ก ชอบเล่นเกม เรียนรู้ทุกอย่าง ชอบเจอคนใหม่ๆ งานสังคมทุกชนิด ออกกำลังกายในวันว่าง อ่านหนังสือ มีเว็บรีวิวหนังสือด้วย www.readraide.in.th