JUnit: spike solution

posted on 02 Sep 2008 05:46 by wonam in softdev

จากตอนที่แล้ว เราพบว่าวิธีการของเรามีปัญหาเมื่อช่วงมีจำนวนเต็มลบด้วย ชุดทดสอบที่ถามหาจำนวนของจำนวนเต็มตั้งแต่ -5 ถึง 10 ที่ 3 หารลงตัว  คำตอบที่เราต้องการคือ 5 แต่เมท็อดที่เขียนกลับคืนค่า 4

ถ้าดูโปรแกรมเราจะพบว่า countToEnd คงมีค่าเท่ากับ 4 แต่ countBeforeBegining มีค่าเท่ากับ 0 (เนื่องจาก f/a = (-5/3) มีค่าเท่ากับ -1) เพื่อให้โปรแกรมของเราทำงานได้ถูกต้อง เราต้องการให้ countBeforeBeginning มีค่า -1

เราจะแก้เมท็อดของเราให้ทำงานได้ถูกต้องได้อย่างไร?

เรามีหลายทางเลือก เช่น หาทางทำให้ countBeforeBeginning มีค่าตามที่เราต้องการ อย่างไรก็ตามเราคงต้องแก้ชื่อตัวแปรด้วย เพราะว่าความหมายมันไม่ตรงกับที่เราต้องการแล้ว เนื่องจากจำนวนข้อมูล "ก่อนจุดเริ่มต้น" ดันมีไม่จำกัดแถมเรายังต้องการให้ผลจากการนับ (count) ของเราเป็นจำนวนลบอีกต่างหาก

สังเกตว่าโปรแกรมเรา "น่าจะ" ทำงานได้ถูกต้องสำหรับช่วงที่มีแต่จำนวนเต็มบวก วิธีที่ผมเลือกก็คือการเลื่อนช่วงที่เป็นข้อมูลป้อนเข้าให้กลายเป็นช่วงที่มีเฉพาะจำนวนเต็มบวก เช่น แทนที่จะถามช่วง [-5,10] เราเปลี่ยนให้ไปถามช่วง [-5+6,10+6] = [1,16] แทน สังเกตว่าผลลัพธ์ที่ได้จะเท่าเดิม

ถ้าจะให้พูดในกรณีทั่วไปก็คือ ถ้าเราต้องการหาจำนวนของจำนวนเต็มตั้งแต่ f ถึง t ที่ a หารลงตัว คำตอบที่ได้จะไม่เปลี่ยนไปถ้าเราเปลี่ยน f เป็น f+ka และเปลี่ยน t เป็น t+ka สำหรับจำนวนเต็ม k ใด ๆ (เช่นในกรณีข้างต้น 6 = 2*3)

คำถามก็คือเราจะหาค่า ka (คือ 6 ในตัวอย่าง) ที่นำมาเลื่อนได้อย่างไร

เวลาเราทำ TDD หลายครั้งเราจะปัญหาที่ทำให้ต้องหยุดพัฒนาโปรแกรมหลักเพื่อไปศึกษาหาทางออก ช่วงที่เราไปศึกษานี้ เราอาจมีการพัฒนาโปรแกรมต้นแบบมาดู โปรแกรมต้นแบบนั้น  ทางกลุ่ม XP จะเรียกว่า spike solution

ดังนั้น (แม้ว่าเมท็อดเราจะเล็กเหลือเกิน) เราก็จะไปทดลองแยกไปเขียนเมท็อด findShift เล่น ๆ ก่อน

อย่างไรก็ตามในการทำ spike solution เราก็จะทำด้วยการ TDD เหมือนเดิม โครงของเมท็อดพร้อมด้วยชุดทดสอบแสดงด้านล่าง

import org.junit.Test;
import static org.junit.Assert.*;

public class ShiftSpike {
	static int findShift(int a, int f) {
		// need real implementation here
		return 6;
	}
	
	@Test
	public void testWhenFIsDivisible() {
		assertEquals(6,ShiftSpike.findShift(3,-6));
	}
	
	@Test
	public void testWhenFIsNotDivisible() {
		assertEquals(9,ShiftSpike.findShift(3,-7));		
	}
}

เราจะเขียนเมท็อดดังกล่าวโดยแบ่งเป็นสองกรณี ขึ้นกับว่า a หาร f ลงตัวหรือไม่ หลังจากแก้ไปแก้มาจนกระทั่งเมท็อดผ่านชุดทดสอบทั้งหมด ได้ผลดังด้านล่าง

	static int findShift(int a, int f) {
		if(f%a==0)
			return -f;
		else
			return a*(1 + (-f)/a);
	}

พอเราได้เมท็อด findShift ที่น่าจะใช้ได้แล้ว เราก็ copy ไปใส่ในคลาส Divisibility แล้วก็แก้เมท็อด countMultiplesInRange ที่เขียนไว้เดิม ดังด้านล่าง

	public static int countMultiplesInRange(int a, int f, int t) {
		if(f<0) {
			int shift = findShift(a,f);
			
			f += shift;
			t += shift;
		}
		
		int countToEnd = 1 + t/a;

		int  countBeforeBeginning = 1 + f/a;
		
		if(f%a==0)
			// fix over counting f when a divides f
			countBeforeBeginning--;   

		return countToEnd - countBeforeBeginning;
	}

เมื่อแก้เรียบร้อย เมท็อดที่ได้อันนี้ก็ผ่านชุดทดสอบเดิมที่เขียนไว้หมด

เรากลับไปดูชุดทดสอบของเราอีกครั้ง เพื่อตรวจสอบความครอบคลุม เราเพิ่มชุดทดสอบอีกชุดที่ขอบเขตของช่วงเป็นจำนวนเต็มลบทั้งหมด ดังด้านล่าง

	@Test
	public void testNegativeRange() {
		assertEquals(2, resultOf(10,-25,-3));
	}

เมื่อทดสอบดูก็พบว่าเมท็อดที่เราเขียนยังทำงานได้อย่างถูกต้อง

เราคงจะหยุดเพิ่มชุดข้อมูลทดสอบไว้ก่อน สังเกตว่ากลุ่มของกรณีทดสอบของเรานั้นมีลักษณะซ้ำกัน โดยจะมีความแตกต่างเฉพาะข้อมูลป้อนเข้าเท่านั้น ครั้งถัดไปเราจะทดลองเขียน parameterized test ที่ทำให้เราสามารถเขียนเมท็อดทดสอบเมท็อดเดียวที่ทำงานกับข้อมูลป้อนเข้าหลาย ๆ ชุดได้ (ดูตัวอย่างการเขียนที่ testearly หรือที่ disco blog ที่มีการเปรียบเทียบกับการเขียนด้วย TestNG)

หมายเหตุ: เขียนไปฟังข่าวไป นึกไม่ออกว่าเรื่องจะจบลงอย่างไร

Comment

Comment:

Tweet