Chapter 20: Debugging and Unit Testing (Part2)

Unit testing with XCTest

ถ้าหากหาทางป้องกันและการหลีกเลี่ยง bug ไว้ตั้งแต่เนิ่นๆ เวลาที่เสียไปกับการ debug ก็จะน้อยลง โปรแกรมมีประสิทธิภาพมากชึ้น หนึ่งในวิธีการป้องกันการเกิด bug คือการใช้ unit testing การทำ unit testing นั้นมีจุดประสงค์เพื่อใช้ทดสอบโค้ดบางส่วนของโปรแกรม เพื่อหาว่าโค้ดส่วนนั้นมีผลลัพธ์ตามที่คาดหวังหรือไม่ ซึ่งต้องอาศัยการเขียนโค้ดขึ้นมาทดสอบหรือที่เรียกว่า test cases ในปัจจุบันมีเฟรมเวิร์คสำหรับ unit testing ให้ใช้มากมายเช่น SenTestingKit , OCTest แต่หลังจาก XCode 5 ได้ออกมา ก็ได้เพิ่ม framework ใหม่เข้ามาสำหรับ unit testing ชื่อว่า XCTest ซึ่งเป็นเฟรมเวิร์คหลักของ Objective-C ในหัวข้อนี้เราจะเขียนโปรแกรมง่ายๆเพื่อใช้สำหรับหัดเขียน unit test โดยมีโค้ดดังต่อไปนี้

Program 20.4
Student.h

Student.m

เมื่อเขียนคลาสทั้งสองเสร็จแล้ว สิ่งที่จะทำต่อไปคือ unit test เพราะต้องการจะทดสอบก่อนว่าคลาสนี้ทำงานถูกต้องตามที่ต้องการ การใช้ unit test สามารถทำได้ด้วยเลือกเมนู File > New > Target … และเลือก Objective-C Unit Testing Bundle ดังรูป แต่ถ้าหากโปรเจคที่สร้างขึ้นมาเป็น iOS App หรือ Mac App จะมี unit test target มาให้เรียบร้อยแล้ว

unit1

เมื่อกด Next จะพบหน้าต่างให้ตั้งชื่อของ Target จากนั้นให้กด Finish ก็เป็นอันเสร็จสิ้นการสร้าง Unit Testing Target

unit2

ถ้าทุกอย่างเรียบร้อยก็จะเห็น folder ใหม่และไฟล์ต่างๆ พร้อมกับ target ใหม่ที่ได้เพิ่มเข้ามา

tartget

ในรูปจะเห็นว่ามีไฟล์ Program_20_4_Tests.m เมื่อเปิดไฟล์ Program_20_4_Tests.m ก็จะเจอโค้ดดังนี้

Program_20_4_Tests.m

ไฟล์ Program_20_4_Tests.m นี้ที่ใช้สำหรับเขียน test case จากโค้ดจะเห็นว่าเทสเคสนี้เป็นซับคลาสของ XCTestCase และมีเมธอดทั้งหมด 3 เมธอดด้วยกันคือ

setUp เมื่อสั่งให้ run test case เมธอดนี้จะถูกเรียกก่อนเสมอ ซึ่งมักใช้สำหรับการกำหนดค่าต่างก่อนจะเริ่มการทดสอบ
tearDown เป็นเมธอดที่จะเรียกหลังจาก test case ทั้งหมดได้ทำงานเสร็จสิ้น
testExample เรียกเมธอดนี้ว่าเป็น test case ซึ่งเมธอดนี้แสดงตัวอย่างวิธีการเขียน test case และเราสามารถประกาศ test case เองได้ โดยการขึ้นต้นชื่อเมธอดด้วย “test” เช่น testSum , testConnection , testValidEmail เป็นต้น และไม่ต้องกำหนดพารามิเตอร์และไม่ส่งค่าใดๆกลับไป

นอกจากไฟล์ Program_20_4_Tests.m แล้ว ยังสามารถเพิ่มไฟล์สำหรับเขียน Test Case โดยเลือกได้จากเมนู File > New > File .. และเลือก Objective-C Test case class

Run test cases

การสั่งให้ชุดทดสอบหรือ test case ทำงานนั้นสามารถทำได้โดยไปที่ Unit Test Navigation ดังรูป และคลิกที่รูปสามเหลี่ยมด้านท้ายของ test case ที่ต้องการ

run

จากนั้น test case จะเริ่มการทดสอบและ XCode จะแสดงผลลัพธ์จากการทดสอบดังนี้

test_fail

เทสเคสที่ได้ทำงานเสร็จสิ้นไป ได้แจ้งว่า testExample นั้นไม่ผ่านการทดสอบ พร้อมกับแสดงข้อความ failed – No implementation for “-[Program_20_4_Tests testExample]” ดังที่เห็นในรูป ไม่ต้องแปลกใจว่าทำไมชุดทดสอบนี้ไม่ผ่าน นั่นเป็นเพราะว่า testExample เรียกใช้ฟังก์ชัน XCFail ซึ่งจะให้ผลลัพธ์ว่าไม่ผ่านหรือเป็น fail เสมอ และในส่วนของ console จะแสดงผล ดังนี้

Test Suite ‘All tests’ started at 2014-02-18 11:02:37 +0000
Test Suite ‘Program 20.4 Tests.xctest’ started at 2014-02-18 11:02:37 +0000
Test Suite ‘Program_20_4_Tests’ started at 2014-02-18 11:02:37 +0000
Test Case ‘-[Program_20_4_Tests testExample]’ started.
/Program_20_4_Tests.m:31: error: -[Program_20_4_Tests testExample] : failed – No implementation for “-[Program_20_4_Tests testExample]”
Test Case ‘-[Program_20_4_Tests testExample]’ failed (0.000 seconds).
Test Suite ‘Program_20_4_Tests’ finished at 2014-02-18 11:02:37 +0000.
Executed 1 test, with 1 failure (0 unexpected) in 0.000 (0.000) seconds
Test Suite ‘Program 20.4 Tests.xctest’ finished at 2014-02-18 11:02:37 +0000.
Executed 1 test, with 1 failure (0 unexpected) in 0.000 (0.000) seconds
Test Suite ‘All tests’ finished at 2014-02-18 11:02:37 +0000.
Executed 1 test, with 1 failure (0 unexpected) in 0.000 (0.003) seconds

รายละเอียดของการทดสอบได้บอก จำนวนของ test case ที่ใช้ในการทดสอบ รวมถึงเวลาที่ใช้สำหรับการทดสอบแต่ละ test case และถ้าหากเทสเคสใด ไม่ผ่านก็ทดสอบก็แจ้ง fail เช่นเดียวกัน

Assert

ในการเขียน test case นั้นจะใช้ชุดคำสั่งที่เรียกว่า XCTAssert เช่น XCTFail ใน testExample คำสั่งเหล่านี้จะเป็นการทดสอบเงื่อนไขตามที่กำหนดเช่น XCTAssertFalse ใช้ทดสอบว่าเงื่อนไขนั้นต้องเป็นเท็จ หรือ XCTAssertEqual ใช้ทดสอบว่าค่าทั้งสองมีค่าเท่ากัน คำสั่ง XCTAssert ที่ใช้บ่อยๆ มีตามตารางดังต่อไปนี้

table

Your First Unit Test

ก่อนที่จะลงมือเขียน unit test แรก ให้ลบเมธอดทั้งสามที่มีอยู่แล้วออกไปเสียก่อน จากนั้นเขียน test case ใหม่ชื่อ testCreateStudent และมีโค้ดดังนี้

Program_20_4_Tests.m

ชุดทดสอบแรกที่เขียนคือ testCreateStudent จากโค้ดได้เรียกใช้ฟังก์ชั่น XCTAssertEqual เพื่อทดสอบว่าค่าของ score จะมีค่าเท่ากับ 60 ตามที่ได้กำหนดหรือไม่ ต่อมาใช้ XCTAssertEqualObject เพ่ือทดสอบว่าชื่อของนักเรียนที่ได้กำหนดให้กับอ็อบเจ็ก student ต้องเป็นค่าเดียวกัน เมื่อให้ test case ทำการทดสอบ ก็จะมักจะเกิดความผิดพลาดดังนี้

build error

สิ่งที่เกิดขึ้นคือโปรแกรมแจ้งเตือนว่ามีความผิดพลาด และความผิดพลาดนี้ไม่ใช่ข้อความแจ้งผลการทดสอบของ unit test เช่นที่ผ่านมา แต่กลับเป็นความผิดพลาดของการคอมไพล์ซึ่ง XCode ได้แจ้งว่า

Undefined symbols for architecture x86_64:
“_OBJC_CLASS_$_Student”, referenced from:
objc-class-ref in Program_20_4_Tests.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

หมายความว่า Program_20_4_Test ไม่รู้จัก symbols ที่ชื่อ _OBJC_CLASS_$_Student หรือพูดง่ายๆว่ามีการเรียกใช้คลาส Student จากไฟล์ Program_20_4_Test แต่กลับไม่เจอคลาส Student นั่นเอง สาเหตุที่ไม่เจอคลาส Student ก็เพราะว่าใน Unit Test Target ไม่ได้กำหนดให้คอมไพล์ไฟล์ Student เข้ามาใน target ด้วย จึงทำให้เกิดข้อผิดพลาด ดังนั้นสิ่งที่ต้องแก้ไขคือกำหนดให้  Unit Test Target นั้นคอมไพล์ไฟล์ Student ด้วย โดยการปรับที่ Build Phase และเพิ่มไฟล์ Student.m

setting

 

เมื่อทดสอบ test case อีกครั้งก็จะได้ผลลัพธ์ดังรูป

pass

สัญลักษณ์สีเขียวด้านหน้าของ test case ได้แสดงว่าการทดสอบผ่านเรียบร้อยดี ต่อไปจะเพิ่มคลาส Course เข้ามาในโปรแกรม ในขั้นตอนของการสร้างคลาสใหม่นี้ให้เลือกทั้งสอง Targets เพื่อจะได้ไม่เกิดปัญหาการคอมไพล์เหมือนที่ผ่านมา

target

คลาส Course ที่จะเขียนต่อไปนี้มีเมธอดให้นักเรียนลงทะเบียน , หาค่าเฉลี่ยคะแนนของนักเรียน , แสดงนักเรียนที่มีผลการเรียนดีที่สุด ,ค้นหานักเรียนจากชื่อได้, บอกจำนวนนักเรียนทั้งชั้น และสุดท้ายแสดงรายชื่อนักเรียนที่สอบผ่าน และมีข้อบังคับคือนักเรียนคนเดิมลงทะเบียนซ้ำไม่ได้, นักเรียนที่ผ่านสอบต้องมีคะแนนมากกว่าหรือเท่ากับ 50 คะแนน

Course.h

Course.m

ดูโค้ดผ่านๆ คลาสที่เขียนขึ้นมานี้น่าจะทำงานได้อย่างถูกต้อง แต่เพื่อความแน่ใจ ก็ต้องเขียนเทสเคสขึ้นมาทดสอบเมธอดต่างๆของคลาส Course ในครั้งนี้ให้สร้างไฟล์เทสเคสขึ้นมาใหม่ โดยเลือก template ของไฟล์เป็น Objective-C Test case class และตั้งชื่อว่า Course_Test จากนั้นเขียน test case ดังต่อไปนี้

Course_Test.m

เนื่องจากคลาส Course ต้องใช้ Student แทบจะทุกเมธอด ดังนั้นใน test case ที่เขียนขึ้นใหม่จึงได้ประกาศ Student เพื่อใช้ในการทดสอบทั้งหมด 5 อ็อบเจ็กด้วยกัน และเขียนส่วนของการสร้างอ็อบเจ็กไว้ที่เมธอด setUp

เทสเคสแรก testSearchStudent ใช้สำหรับทดสอบการหาอ็อบเจ็ก student จากชื่อที่กำหนด ถ้าหากไม่มีอ็อบเจ็กนั้นอยู่ในรายชื่อ สิ่งที่คาดหวังคือเมธอดต้องส่งค่า nil กลับมา

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

เทสเคส testMaxScore และ testAverage ใช้สำหรับการทดสอบคะแนนมากที่สุด และหาคะแนนเฉลี่ยของนักเรียน

และสุดท้าย testPassedStudent ไว้ทดสอบผู้ที่มีคะแนนผ่านเกณฑ์ที่กำหนด เมื่อเขียนเทสเคสเสร็จเรียบร้อย จากนั้นให้ทำการทดสอบ test case ที่ได้เขียนไป ก็จะพบกับผลลัพธ์ดังนี้

Test Case ‘-[Course_Tests testAverage]’ started.
/Course_Tests.m:78: error: -[Course_Tests testAgerage] : (([course averageScore]) equal to (0)) failed: (“nan”) is not equal to (“0”) – average score fail

Test Case ‘-[Course_Tests testPassedStudent]’ started.
/Course_Tests.m:97: error: -[Course_Tests testPassedStudent] : (([passed count]) equal to (4)) failed: (“3”) is not equal to (“4”) – passedStudent fail

Test Case ‘-[Course_Tests testSearchStudent]’ started.
/Course_Tests.m:51: error: -[Course_Tests testSearchStudent] : ((studentB) == nil) failed: “Weerapong : 65” – search fail

ผลของการทดสอบมี test case ที่ไม่ผ่านถึง 3 เคสด้วย โดยเคสแรก testAverage ได้แจ้งว่าทดสอบไม่ผ่านเพราะเกิดค่า “nan” ค่านี้จะเกิดขึ้นได้ก็ต่อเมื่อหารด้วย 0 เมื่อย้อนกลับไปดูเมธอด averageScore จะเห็นว่ามีข้อผิดพลาดคือ ในกรณีที่ไม่มีนักเรียนอยู่เลยจำนวนนักเรียนจะเป็น 0 ดังนั้นโปรแกรมจะหารด้วย 0 นั่นเอง การแก้ปัญหานี้คือให้ส่งค่า 0 กลับเมื่อ studentList ไม่มีอ็อบเจ็ก Student อยู่เลย

เทสเคสต่อมาคือ testPassedStudent ซึ่งได้คาดหวังผลลัพธ์ว่าจำนวนนักเรียนที่ผ่านสอบควรจะเป็น 4 แต่ผลการทดสอบแจ้งว่าเป็น 3 เมื่อพิจาณาจากโค้ดของเมธอด passedStudent จะเห็นว่าเงื่อนไข score > 50 ที่ใช้ในการเปรียบเทียบนั้นผิด ทำให้นักเรียนที่มีคะแนน 50 สอบตก เงื่อนไขที่ถูกต้องคือ score >= 50
ส่วนเคสสุดท้าย testSearchStudent สิ่งที่ต้องการคือหานักเรียนชื่อ Weera โดยผลลัพธ์ที่คาดหวังคือต้องไม่เจอนักเรียนคนดังกล่าว แม้ว่าโปรแกรมจะไม่เจอ Weera แต่จากผลการทดสอบก็ยังพบข้อผิดพลาด เพราะเมธอดได้พบนักเรียนชื่อ Weerapong เมื่อกลับไปดูโค้ด studentWithName: ก็จะเห็นว่า predicate ที่ใช้คือ @”name contains %@” นั้นไม่ถูกต้อง เพราะเป็นการหาชื่อที่มีส่วนใดส่วนหนึ่งประกอบด้วยคำที่กำหนด ดังนั้นเมื่อค้นหา Weera ก็จะได้ Weerapong ด้วย เพราะ Weerapong มีคำว่า “Weera” อยู่นั่นเอง เราควารแก้ไขเงื่อนไขให้เป็น @”name contains %@” โปรแกรมก็จะทำงานได้อย่างถูกต้อง

จากผลของการทดสอบเห็นได้ว่า เมื่อดูผ่านๆแล้วคลาส Course น่าจะทำงานได้อย่างถูก แต่กลับมีข้อผิดพลาดในโปรแกรมถึง 3 แห่งด้วยกัน การใช้ Unit test ยังมีข้อดีอีกอย่างก็คือ เมื่อไหร่ก็ตามที่แก้ไขโค้ดของโปรแกรม เช่น แก้ไขการเปรียบเทียบในเมธอด maxScore ให้เป็น

เมื่อให้ชุดทดสอบได้ทำงานอีกครั้ง ก็จะพบว่าเมธอดนี้ไม่ผ่านการทดสอบ การใช้ Unit Test นั้นอาจจะดูเป็นเรื่องไม่จำเป็น เพราะต้องเขียนโค้ดที่ใช้ทดสอบมากมายหลายอย่างด้วยกัน และยังต้องคิด test case เพื่อใช้ในการทดสอบ แต่การใช้ Unit Test นั้นช่วยป้องกันปัญหาความผิดพลาดของโปรแกรมในระยะยาวได้เป็นอย่างดี โดยเฉพาะเมื่อต้องเขียนโปรแกรมขนาดใหญ่ และมีโปรแกรมเมอร์หลายคนในโปรเจค  การใช้ Unit Test จะเป็นสิ่งช่วยป้องกันความผิดพลาดในโปรแกรมที่จะเกิดขึ้นได้เป็นอย่างดี

Summary

 

สิ่งที่ได้เรียนรู้ในบทนี้ แม้ไม่เกี่ยวกับภาษา Objective-C โดยตรง แต่ก็เป็นประโยชน์สำหรับการเขียนโปรแกรมอย่างแน่นอน เพราะนอกจากความรู้ความเข้าใจเกี่ยวกับภาษาแล้ว ยังต้องมีทักษะอย่างอื่นเช่นการ debug หรือการใช้ unit testing หนังสือเล่มนี้ไม่อาจจะอธิบายการใช้งานต่างๆได้หมดเช่น การใช้ lldb command ซึ่งมีประโยชน์อย่างมากในการ debug สำหรับผู้ที่สนใจสามารถศึกษาเพิ่มเติมได้จากเอกสารของโครงการ lldb โดยตรง

It just beginning not the end

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

สุดท้ายนี้มีคำพูดหนึ่งที่ สตีฟ จ็อบ ได้กล่าวตอนให้สุนทรพจน์แก่นักศึกษา Stanford University ที่จบใหม่ และผมก็จะขอนำคำพูดนั้นมาพูดกับคุณอีกครั้ง
“Stay foolish Stay hungry”

 

โหลด PDF ไปอ่านได้ครับ

ส่วน Source code ก็เช่นเดิม โหลดได้ที่ github

Leave a Reply