Wednesday, February 29, 2012

Test Driven Development in Thai

Test Driven Development หรือ TDD คือระเบียบวินัยการออกแบบซอฟต์แวร์ โดยไม่เริ่มจากการออกแบบโคด แต่เริ่มด้วยการเขียนบททดสอบ นัยหนึ่ง การทดสอบก็ถือได้ว่าเป็นการออกแบบโคดนั่นเอง แต่เป็นการออกแบบที่สามารถนำมาใช้ยืนยันพฤติกรรมของซอฟต์แวร์ได้จริงโดยอัตโนมัติ

Testing มีจุดประสงค์ก็คือ
1) เป็นการกำหนดสเปคของโคดที่เราจะเขียน ว่าเราต้องการให้มันมีกลไกอย่างหนึ่ง
2) ยืนยันว่ากลไกที่เราเขียนออกมานั้นถูกต้อง
3) ป้องกันโคดเดิม ไม่ให้โคดใหม่มาเปลี่ยนกลไกโดยที่เราไม่ได้ตั้งใจ
4) เป็นหนังสืออ้างอิง สำหรับเพื่อนร่วมงานหรือลูกค้าที่จะมีความจำเป็นจะต้องศึกษาระบบเพื่อรับช่วงต่อ

เพื่อให้บรรลุจุดประสงค์เหล่านี้ ก็เลยมีระเบียบวินัยบางประการครับ ระเบียบวินัยเหล่านี้ก็คือ
1) เทสต้องแยกเป็นหน่วยย่อย เพื่อกำหนดวัตถุประสงค์ที่เป็นหน่วยเล็กที่สุดที่สมเหตุสมผล
2) เมื่อพร้อมจะคอมมิทงานแต่ละครั้ง เทสทุกเทสจะต้องผ่านหมด
3) ถ้างานที่คอมมิทไปทำให้เทสอื่นไม่ผ่าน ทั้งทีมต้องไม่คอมมิท จนกว่าคนที่คอมมิทข้อผิดพลาดจะแก้ไขเสร็จ และคอมมิทงานที่ซ่อมแซมแล้ว มิฉะนั้นข้อผิดพลาดอาจมีงานอื่นมาซ้อนทับ ทำให้การแก้ไขไม่มีที่สิ้นสุด
4) เทสต้องอ่านง่าย และสมดุลกับโคด ไม่ยืดเยื้อ แต่ก็ครอบคลุมกลไกครบถ้วน
5) สามารถทำซ้ำได้ทุกครั้งโดยที่ผลลัพธ์ต้องเหมือนเดิมทุกครั้งหากไม่มีความเปลี่ยนแปลงใดๆ เกิดขึ้นกับโคดหรือบททดสอบ

เมื่อเรามีวัตถุประสงค์ กับระเบียบวินัยดังนี้ การปฏิบัติก็มีหลักการชัดเจนขึ้น เราก็สามารถออกแบบระบบการทดสอบได้ดังนี้

กำหนดขอบเขต ตามระดับความครอบคลุม เราสามารถแบ่งการทดสอบเป็นระดับ ลดหล่นลงมาได้

จากระดับสูงสุดคืองานทดสอบเพื่อยอมรับการส่งมอบงาน (acceptance testing) โดยการทดสอบแต่ละหน่วย จะต้องบรรลุถึงจุดประสงค์ทางธุรกิจ (business value) แต่ละข้อที่กำหนดไว้ เช่น
1) สามารถลดเวลาการทำงานของฝ่ายสินเชื่อลงจากสามวันเป็นหนึ่งวัน
2) โดยเมื่อปฏิบัติตามขั้นตอนที่ออกแบบไว้จะสามารถดำเนินการตั้งแต่ลูกค้าส่งใบขอสินเชื่อจนถึงแจ้งลูกค้าถึงการอนุมัติ หรือปฏิเสธสินเชื่อดำเนินการไปอย่างราบรื่นโดยไม่มีข้อผิดพลาด
3) ฝ่ายการเงินต้องรับทราบข้อมูลจากฝ่ายสินเชื่อภายในเวลาที่เหมาะสมเพื่อพิจารณาแผนการปฏิบัติงานต่อไป โดยข้อมูลที่ต้องการประกอบด้วย […]

รองลงมาเป็นการทดสอบแบบ บุรณาการ (Integration Testing) เป็นการทดสอบระบบทั้งหมด ให้การทำงานเป็นไปตามความต้องการที่ออกแบบไว้ เช่น ทดสอบว่า
1) application ของเรา สามารถดึงข้อมูลจากธนาคารเพื่อตรวจสอบหลักทรัพย์ ก่อนอนุมัติเงินกู้
2) เมื่ออนุมัติเงินกู้แล้ว application ของเรา ส่งอีเมลถึงลูกค้า และส่งสัญญาณแจ้ง application ของฝ่ายการเงิน เพื่อแจ้งให้ทราบต่อไป

ระดับรองลงมา เป็นการทดสอบการใช้งานจริง ที่ไม่ครอบคลุมถึงปัจจัยภายนอก (Functional Testing) ระดับนี้จะครอบคลุมถึงการใช้งานจริงของ application ในระดับผู้ใช้ ลงมือใช้งาน application ของเรา ตัวอย่างเช่น
1) ถ้า log in ด้วยข้อมูลที่ถูกต้อง หน้าจอจะแสดงหน้าหลัก บอกชื่อ และเวลาที่ผู้ใช้เข้าใช้งานครั้งสุดท้าย
2) เมื่อผู้ใช้กรอกแบบฟอร์มครบถ้วนและกดตกลง application จะเรียกใช้ server แบบ AJAX, แสดง progress bar เพื่อแสดงความคืบหน้าในการดำเนินการ เมื่อดำเนินการเสร็จแล้ว จะมีบันทึกการขอสินเชื่อของผู้ใช้
3) เมื่อสินเชื่ออนุมัติ และผู้ใช้เข้าใช้งาน จะมีกรอบข้อความแจ้งเตือนถึงการอนุมัตินี้

เมื่อเราแยกย่อยลงมาอีก ก็จะเป็น การทดสอบหน่วยการทำงาน (Unit Testing) ระดับนี้จะลงรายละเอียดของออบเจคต์แต่ละส่วน เมธอดแต่ละส่วน ว่ามีกลไกเป็นอย่างไร ตัวอย่างเช่น
1) ออบเจคต์สินเชื่อ ต้องมีความสัมพันธ์กับออบเจคต์ผู้ใช้ 1 คนเท่านั้น ไม่สามารถมีสินเชื่อ ที่ไม่มีผู้ใช้อยู่ในระบบได้
2) เมธอด อนุมัติ มีหน้าที่สามประการ
2.1) ส่งอีเมลแจ้งลูกค้า
2.2) เรียก web service ของฝ่ายการเงิน
2.3) บันทึกข้อมูลข่าวสาร เพื่อแจ้งให้ลูกค้าทราบผ่านทางหน้าเวบ
3) วิธีการเรียก web service ของฝ่ายการเงิน
3.1) ใช้ RESTful protocol โดยมี URL คือ POST http://example.com/mortgages/approved
3.2) body ของ request เข้ารหัสด้วย JSON และมีโครงสร้างดังนี้ {…}
3.3) เมื่อฝ่ายการเงินได้รับข้อความแล้ว จะตอบกลับมาด้วย Header 200
3.4) หากมีข้อผิดพลาด ฝ่ายการเงินจะตอบกลับมาด้วย Header 403 และใน body จะมีออบเจคต์เข้ารหัสด้วย JSON และมีโครงสร้างดังนี้ {…}

เมื่อเรากำหนดขั้นตอนการทำงานได้ดังนี้แล้ว เราสามารถทำได้สองทิศทางคือ บนลงล่าง หรือล่างขึ้นบน แล้วแต่ความสมเหตุสมผลงานงานหน่วยนั้นๆ

การทดสอบแบบล่างขึ้นบน คือการเริ่มจาก Unit testing ก่อน ออกแบบออบเจคต์ของเราให้มีกลไกที่เรียบง่ายที่สุดเพื่อที่จะส่งเสริมให้เราสามารถออกแบบ tests ที่ระดับสูงขึ้นได้ จากนั้นค่อยยกระดับการทดสอบขึ้น เป็น functional แล้วค่อยเป็น integration

การทดสอบแบบบนลงล่าง ก็เริ่มจากระดับบนสุดก่อน ถ้ามีปัจจัยภายนอก ก็เริ่มที่ integration แล้วเมื่อเห็นความต้องการว่ามีความจำเป็นจะต้องมี function ดังนี้ๆ, ก็เริ่มเขียน functional ตามความจำเป็น แล้วก็ย่อยลงเป็น unit test ตามความจำเป็นเช่นกัน

ทั้งสองแบบมีข้อดีข้อเสียด้วยตัวมันเอง การทดสอบแบบล่างขึ้นบน เหมาะสำหรับระบบใหม่ ที่ยังไม่มีข้อจำกัดใดๆ ทำให้ระบบภายในเรียบง่ายและชัดเจน แต่มีความเสี่ยงที่จะออกแบบล่วงหน้ามากเกินไป หรือไม่ตรงตามความจำเป็นที่เกิดขึ้นเมื่อทดสอบในระดับที่สูงขึ้น

ส่วนการทดสอบแบบบนลงล่าง เหมาะสำหรับระบบที่มีปัจจัยภายนอกที่ชัดเจน เช่น ข้อกำหนดของ web service, หรือหน้า User Interface และ workflow ที่ชัดเจน ทำให้เราสามารถกำหนดขอบเขตชัดเจนว่าความจำเป็นของชิ้นส่วนต่างๆ เกิดจากความจำเป็นจริงๆ แต่การออกแบบเพื่อให้เข้ากับระบบที่มีอยู่เดิม จะทำได้ยากกว่าเพราะการทดสอบระดับสูงคำนึงถึงกลไกเบื้องล่างน้อยกว่ามาก

การทดสอบที่ไม่สมเหตุสมผลคือการทดสอบที่ไม่ต่อเนื่อง ไม่สอดคล้องกับข้อกำหนดในเนื้องาน และการทดสอบระบบที่เราไม่มีหน้าที่และความรับผิดชอบ เช่น library จากบุคคลที่สาม การทดสอบที่ผิดพลาด การทดสอบที่ไม่ครอบคลุมกลไกอย่างครบถ้วน สิ่งเหล่านี้ทำให้การทดสอบเชื่อถือไม่ได้ เมื่อเชื่อถือไม่ได้ ก็ไม่มีประโยชน์ ก็จะไม่มีการบำรุงรักษา เป็นวงจรที่ขยายวงกว้างอย่างรวดเร็ว ทำให้งานทั้งระบบ ขาดข้อยืนยันว่าถูกต้อง ความน่าเชื่อถือ และเอกสารอ้างอิง ในที่สุดก็จะไม่สามารถใช้งานได้อย่างยั่งยืนต่อไป

ส่วนประกอบของการทดสอบในทางเทคนิคมีสามประการหลักๆ คือ
1) สถานะเริ่มต้น (setup)
2) เรียกใช้กลไกที่ต้องการทดสอบ (performance)
3) รับรองว่าสถานะที่เปลี่ยนไป สอดคล้องกับพฤติกรรมที่เราต้องการจากกลไกนั้น (assertion)

การกำหนดสถานะเริ่มต้นทำให้เราทราบสถานะที่แน่นอนของระบบในขณะนั้น เราจะต้องทำให้ไม่มีปัจจัยภายนอกใดๆ ที่ทำให้มีความไม่แน่นอนส่งผลกับกลไกที่กำลังทดสอบนั้น

การเรียกใช้กลไกที่จะออกแบบ ขึ้นอยู่กับระบบ แต่ต้องเรียกใช้ในส่วนที่เฉพาะเจาะจงที่สุดที่สมเหตุสมผล เช่น เรียกใช้เมธอดนั้นโดยเฉพาะในการทดสอบ unit testing หรือการเรียก controller action โดยตรงในการทดสอบ functional testing หรือการเลือกคลิกปุ่มเฉพาะเจาะจงในการทดสอบ integration testing เป็นต้น

การรับรองสถานะ ควรทำให้เฉพาะเจาะจงแยกเป็นแต่ละกรณีไป เช่นเมื่อเรียกเมธอดเดียวกัน อาจต้องมีการรับรองสามอย่าง ให้แยกทดสอบทีละอย่าง โดยสามารถออกแบบให้การสร้างสถานะเริ่มต้นและการเรียกใช้งานกลไก เป็นเมธอดที่สามารถเรียกซ้ำได้

ข้อสำคัญที่ต้องระวัง คือสถานะผลลัพธ์จากการทดสอบหนึ่ง อาจส่งผลกระทบกับการทดสองอีกชุดหนึ่ง ซึ่งข้อผิดพลาดนี้จะไม่แสดงให้เห็นชัดเจน บางครั้งกว่าจะตรวจพบ ก็เมื่อได้ใช้งานจริงแล้ว

คำถามที่น่าสนใจคือ แล้วจะทราบได้อย่างไร ว่าการทดสอบที่เราเขียนนั้นถูกต้อง แล้วเราจะทดสอบการทดสอบการทดสอบโดยไม่มีที่สิ้นสุด หรืออย่างไร? คำตอบคือ โคดของเรา จะเป็นตัวทดสอบบททดสอบด้วยเช่นกัน หากการทดสอบผิดพลาด แต่เราทบทวนจนแน่ใจแล้วว่าโคดของเรา สมเหตุสมผลสามารถยืนยันได้ด้วยทฤษฏีหรือวิธีอื่นๆ เราก็สามารถตั้งข้อสงสัยได้ว่าการทดสอบของเราผิดหรือไม่

เมื่อเริ่มเขียนบททดสอบ ต้องเขียนให้แน่ใจว่าการทดสอบนี้ ต้องแจ้งให้เราทราบว่าโคดมีข้อผิดพลาด ก่อนที่เราจะเริ่มเขียนโคด มิฉะนั้นจะไม่มีทางทราบว่าโคดที่เราเขียนใหม่นี้ เป็นส่วนที่ทำให้การทดสอบของเราผ่านจริง (red bar)

จากนั้นจึงค่อยเริ่มเขียนโคด ให้ง่ายที่สุด ที่สมเหตุสมผล และทำให้การทดสอบของเราผ่าน (green bar)

จากนั้นค่อยพิจารณา ว่าโคดของเราสามารถปรับปรุงให้ดีขึ้นได้หรือไม่ สามารถ refactor ให้ไม่ซับซ้อน อ่านง่าย เข้าใจง่าย มีประสิทธิภาพ และไม่ซ้ำซ้อนกับกลไกอื่นได้หรือไม่ ทั้งนี้เรามีหลักประกันจากการทดสอบ ว่าพฤติกรรมของโคดจะต้องไม่เปลี่ยนแปลงในช่วง refactor นี้

จากนั้นก็ทำเป็นวงจรต่อเนื่องไปเรื่อยๆ red bar, green bar, refactor จนกว่าจะบรรลุวัตถุประสงค์ของโปรแกรมตามต้องการ

จะเห็นได้ว่า ระเบียบวินัยแบบ TDD นี้ สามารถนำไปใช้กับซอฟต์แวร์ทุกระดับ ตั้งแต่เวบเล็กๆ ไปจนถึงระบบ ERP ขององค์กรระดับชาติ ทำให้ซอฟต์แวร์มีความน่าเชื่อถือ ตรงตามความต้องการ ไม่ขาดไม่เกิน บำรุงรักษาได้ มีเอกสารอ้างอิงชัดเจน และเป็นการบังคับให้เปลี่ยนเอกสารอ้างอิงทุกครั้งที่พฤติกรรมของซอฟต์แวร์เปลี่ยนไปจากเดิม ทำให้เอกสารอ้างอิงนี้ใช้ประโยชน์ได้จริงอยู่เสมอ

No comments: