Chapter 20: Debugging and Unit Testing (Part1)

Debugging & Unit Testing

 

Bug คือข้อผิดพลาดที่ทำให้โปรแกรมทำงานไม่เป็นไปตามที่โปรแกรมเมอร์สิ่งกำหนด การเกิด bug อาจจะส่งผลกระทบต่อโปรแกรมเล็กน้อย และอาจทำให้เกิด error ในบางฟังก์ชัน หรือถ้าร้ายแรงมากก็ทำให้โปรแกรม crash และปิดตัวลง เคยมีคนได้กล่าวไว้ว่า “โปรแกรมที่ไม่มี bug คือโปรแกรมที่ยังไม่ได้เขียน” ไม่ว่าจะเขียนโปรแกรมง่ายหรือยาก ต่อให้เป็นโปรแกรมเมอร์เก่งแค่ไหนก็ต้องเขียนโปรแกรมผิดพลาดบ้าง ซึ่งถือเป็นเรื่องปกติธรรมดา เมื่อเกิด bug สิ่งที่โปรแกรมเมอร์ต้องทำก็คือการหาข้อผิดพลาดและแก้ไขหรือที่เรียกว่า Debug ทักษณะนี้จำเป็นอย่างยิ่งที่โปรแกรมเมอร์จะต้องเรียนรู้ และฝึกฝนให้ชำนาญ ยิ่งมีประสบการณ์ debug มากเท่าไหร่ ก็ยิ่งได้เปรียบมากเท่านั้น เพราะในบางครั้งเวลาที่ใช้สำหรับการ debug นั้นมากกว่าเวลาที่ใช้เขียนโปรแกรมเสียอีก เนื้อหาในบทนี้จะอธิบายถึงการใช้ debugger รวมถึงเทคนิควิธีการขั้นสูงที่ใช้ในการ debug เช่น exception breakpoint

LLDB

Debugger คือเครื่องมือที่ใช้สำหรับการ debug แต่เดิมทีนั้น debugger ที่ใช้ใน XCode คือ GNU Debugger หรือเรียกสั้นๆว่า GDB เป็นดีบั๊กเกอร์์มาตรฐานที่ใช้กันทั่วไปในระบบปฎิบัติการ linux , unix แต่ด้วยข้อจำกัดหลายๆอย่างของ gdb จึงได้เกิดการพัฒนาดีบั๊กเกอร์ใหม่ขึ้นมาภายใต้โครงการเดียวกันกับ llvm โดยมีชื่อว่า lldb และมากไปกว่านั้น lldb ไม่ได้เป็นเพียงแค่ดีบั๊กเกอร์ที่มีความสามารถเพิ่มมากขึ้น แต่ lldb ได้ถูกออกแบบมาให้เป็น framework โดยมี API ให้ใช้งานผ่านทางภาษา python ดีบั๊กเกอร์ lldb นี้ได้ถูกนำมาใช้งานร่วมกับ XCode ครั้งแรกใน XCode 4.3 โดยให้ผู้ใช้เลือกได้ว่าจะใช้ gdb หรือ lldb อย่างไรก็ตามตั้งแต่ XCode 5 เป็นต้นมา lldb ได้กลายเป็นดีบักเกอร์หลักของ XCode โดยทั่วๆไปการดีบั๊กแอปพิเคชั่นจะทำผ่านทาง user interface ของ XCode เช่น การวาง breakpoint อย่างไรก็ตามเราสามารถใช้ lldb ได้โดยตรงผ่านทาง debug console (หนังสือเล่มนี้ไม่ได้ครอบคลุมการใช้ lldb command)

Breakpoint

การดีบั๊กนั้นสามารถทำได้หลายวิธีหนึ่งในวิธีง่ายที่สุดคือการใช้ NSLog เพื่อแสดงค่าของตัวแปรที่ต้องการ หากโปรแกรมง่ายไม่ซับซ้อน ก็อาจจะใช้ NSLog ได้ แต่เมื่อโปรแกรมใหญ่และซับซ้อนขึ้น การใช้ NSLog อย่างเดียวนั้นไม่เพียงพอต่อการแก้ไข bug ที่เกิดขึ้นได้ การใช้ debugger เป็นทางออกที่ดีกว่าการใช้ NSLog เพราะดีบั๊กเกอร์สามารถวาง breakpoint เพื่อให้โปรแกรมหยุดทำงานชั่วขณะ จากนั้นก็จะสามารถดูค่าของตัวแปรต่างในขณะนั้นได้ทันที การกำหนด breakpoint นั้นสามารถทำได้ง่ายๆ ด้วยการคลิ๊กที่ด้านหน้าของบรรทัดที่ต้องการ จากนั้นก็จะเห็นสัญลักษณ์ ลูกศรสีฟ้าเข้ม

breakpoint_basic

หากต้องการจะลบ breakpoints ทำได้ด้วยการกดค้างที่เบรกพ้อนต์และลากทิ้ง หรือจะลบจาก Breakpoints Navigation ดังที่แสดงในรูป ได้เช่นเดียวกัน

breakpoint_nav

ในกรณีที่ไม่ต้องการให้เบรกพ้อนต์ทำงานชั่วคราว (disable) ก็ทำได้ด้วยการกดคลิ๊กที่ breakpoints ที่ต้องการอีกครั้งหนึ่ง จากนั้นสัญลักษณ์จะเปลี่ยนเป็นสีฟ้าอ่อน

disable_breakpoint

เมื่อสั่งให้โปรแกรมทำงาน โปรแกรมจะหยุดยังบรรทัด breakpoint ที่ได้กำหนดไว้ จากนั้นก็จะสามารถดูค่าตัวแปรต่างๆได้จาก debug area ดังที่แสดงในรูป

debug_consol

จากรูปข้างบน ข้อมูลที่ XCode แสดงจะประกอบไปด้วย

  • Call stack ข้อมูลในส่วนนี้จะแสดงเมธอดที่ทำงานในขณะนั้น และบอกถึงเทรดที่เมธอดทำงานอยู่ รวมถึงบอกว่าเมธอดนี้ถูกเรียกมาจากที่ใด เมื่อดูจากรูปจะเห็นว่าเมธอด +[Math isPrime:] คือเมธอด ณ จุดที่โปรแกรมทำงานกำลังทำงาน ซึ่งเรียกมาจาก main อีกทอดหนึ่ง และเมธอดนี้ทำงานที่ Thread 1 นั่นเอง
  • Variable ตรงส่วนนี้จะแสดงข้อมูลของตัวแปรรวมถึงพารามิเตอร์ที่เกี่ยวข้องกับเมธอดหรือฟังก์ชั่นที่ทำงานในขณะนั้น เรียกอีกอย่างว่า Frame จากตัวอย่างในรูปจะเห็นว่า frame ประกอบไปด้วย isPrime , num และ self เท่านั้นไม่ได้แสดงตัวแปรอื่นที่ไม่เกี่ยวข้องกับ frame นี้เลย (ในกรณีที่มี global variable ก็จะแสดงตัวแปรนั้นด้วย)
  • Debug console ไว้ใช้แสดงผลลัพธ์ต่างๆที่แจ้งโดย debugger และยังสามารถใช้พิมพ์คำสั่ง debugger command ผ่านทาง console นี้ได้โดยตรงอีกด้วย
  • Debug Navigation เมื่อโปรแกรมหยุดที่เบรกพ้อนต์ โปรแกรมเมอร์สามารถสั่งให้โปรแกรมทำงานต่อไปได้ด้วยการกดปุ่ม continue , step over , step into , step out
  1. continue เป็นคำสั่งให้โปรแกรมจะทำงานต่อไป ซึ่งจะหยุดอีกครั้งก็ต่อเมื่อเจอ breakpoint
  2. step over เมื่อใช้คำสั่งนี้โปรแกรมจะทำงานต่อไปอีก 1 คำสั่ง
  3. step into ใช้เพื่อเข้าไปดูการทำงานภายในของฟังก์ชั่นหรือเมธอด แต่ในกรณีที่ไม่มี source code โปรแกรมจะแสดงโค้ดด้วยภาษา assembly แทน
  4. step out ใช้เพื่อกลับไปยังเมธอดที่ถูกเรียกเข้ามาก่อนหน้านี้
  5. และสุดท้ายคือปุ่ม simulate location ไว้สำหรับจำลองตำแหน่ง GPRS ของ iOS Application

Exception Breakpoints

นอกจากการวาง breakpoint แบบปกติทั่วไป ที่ได้อธิบายไป ยังมีเบรกพ้อนต์แบบพิเศษที่เรียกว่า Exception Breakpoint ให้ลองพิจารณาโปรแกรมต่อไปนี้

เมื่อสั่งให้ทำงาน โปรแกรมจะเกิดข้อผิดพลาดขึ้นและปิดตัวลง เพราะอ็อบเจ็ก b มีค่าเป็น nil ทำให้เมธอด addObject: นั้นทำงานผิดพลาด และ debug console จะแจ้งข้อผิดพลาดดังนี้

*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil’
*** First throw call stack:
(
0   CoreFoundation    0x00007fff84e9141c __exceptionPreprocess + 172
1   libobjc.A.dylib    0x00007fff84fe3e75 objc_exception_throw + 43
2   CoreFoundation    0x00007fff84d528b7 -[__NSArrayM insertObject:atIndex:] + 951
3   Program 20.1        0x0000000100000ec1 main + 177
4   libdyld.dylib    0x00007fff898985fd start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

ข้อความที่แจ้งมาคือ เกิดข้อผิดพลาดจากการเรียกเมธอด insertObject:atIndex: เนื่องจากอ็อบเจ็กเป็น nil (โค้ดโปรแกรมใช้เมธอด addObject: แต่เมธอดนี้จะเรียกใช้ insertObject:atIndex อีกทีดังนั้นจึงแจ้งข้อผิดพลาดที่เมธอดนี้แทน) และ XCode ก็ได้แสดงตำแหน่งของโค้ดที่เกิดข้อผิดพลาดขึ้น

bug1

การแก้ไข bug ในโปรแกรมนี้ทำงานได้ง่ายมากเพราะ XCode ได้หยุดโปรแกรมไว้ยังบรรทัดที่เกิดข้อผิดพลาด แต่ถ้าหากโปรแกรมมีซับซ้อนมากขึ้น เช่น โปรแกรมต่อไปนี้

Program 20.1
main.m

 

เมื่อให้สั่งให้โปรแกรมทำงาน debug console จะแสดงผลดังนี้

*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil’
*** First throw call stack:
(
0   CoreFoundation    0x00007fff84e9141c __exceptionPreprocess + 172
1   libobjc.A.dylib    0x00007fff84fe3e75 objc_exception_throw + 43
2   CoreFoundation    0x00007fff84d528b7 -[__NSArrayM insertObject:atIndex:] + 951
3   Program 20.1        0x0000000100001cf5 __main_block_invoke13 + 101
4   Foundation        0x00007fff888b5055 -[NSBlockOperation main] + 75
5   Foundation        0x00007fff88894591 -[__NSOperationInternal _start:] + 631
6   Foundation        0x00007fff8889423b __NSOQSchedule_f + 64
7   libdispatch.dylib    0x00007fff86af22ad _dispatch_client_callout + 8
8   libdispatch.dylib    0x00007fff86af67ff _dispatch_async_redirect_invoke + 154
9   libdispatch.dylib    0x00007fff86af22ad _dispatch_client_callout + 8
10  libdispatch.dylib    0x00007fff86af409e _dispatch_root_queue_drain + 326
11  libdispatch.dylib    0x00007fff86af5193 _dispatch_worker_thread2 + 40
12  libsystem_pthread.dylib    0x00007fff84179ef8 _pthread_wqthread + 314
13  libsystem_pthread.dylib    0x00007fff8417cfb9 start_wqthread + 13
)
libc++abi.dylib: terminating with uncaught exception of type NSException

โปรแกรมได้แจ้งว่าเกิดข้อผิดพลาดที่เมธอด insertObject:atIndex: เช่นเดียวกันกับโปรแกรมที่แล้ว แต่เมื่อดูจาก debug area สิ่งที่ XCode ได้แสดงกลับแตกต่างจากไปโปรแกรมที่แล้ว

debug_area

จะเห็นว่า XCode ไม่ได้แจ้งบรรทัดที่เกิดข้อผิดพลาดเลยแต่กลับแสดงโค้ดด้วยภาษา assembly แทน นั่นเป็นเพราะว่าแท้ที่จริงแล้ว XCode จะหยุดเมื่อได้รับข้อผิดพลาดหรือ catch exception ไม่ใช่หยุดที่จุดเกิด exception (throw exception) เมื่อกลับไปพิจารณาโค้ดใหม่จะเห็นว่าในบล็อก one นั้น อาเรย์ได้รับเอาตัวแปรที่มีค่าเป็น nil จึงทำให้โปรแกรมนี้เกิดข้อผิดพลาดนั่นเอง โปรแกรมตัวอย่างแก้ไขง่ายเพราะมีโค้ดเพียงไม่กี่บรรทัด แต่เมื่อเขียนโปรแกรมเป็นร้อย เป็นพันบรรทัดขึ้นไป จะรู้ได้อย่างไรว่าผิดพลาดที่ส่วนใดของโปรแกรม ? วิธีการแก้ปัญหาก็คือใช้ Exception Breakpoint เข้ามาช่วย ซึ่งเบรกพ้อนต์ชนิดนี้จะให้โปรแกรมหยุดทำงานเมื่อเกิด exception วิธีการสร้าง exception breakpoint สามารถทำได้โดยเลือกเมนู Debug > Breakpoints > Create Exception Breakpoints หลังจากนั้น ก็จะเห็น exception breakpoint ที่ได้สร้างขึ้นโผล่เข้ามายัง Breakpoint Navigation ดังรูป

add_exception

จากรูปมี breakpoint ในโปรแกรมทั้งหมด 2 breakpoint คือในบรรทัดที่ 17 และ exception breakpoint (สัญลักษณ์ Ex) เมื่อสั่งให้โปรแกรมทำงานอีกครั้ง XCode จะหยุดโปรแกรมยังบรรทัดที่เกิดข้อผิดพลาด เท่านี้ก็แก้ไขปัญาที่เกิดขึ้นได้แล้ว

Symbolic Breakpoints

นอกจาก breakpoint แบบปกติและ exception breakpoint ที่ได้อธิบายไป ยังเหลือหนึ่งอย่างนั่นคือ symbolic breakpoint ซึ่งเป็นการกำหนด breakpoint โดยใช้ symbolic การกำหนด symbolic มีด้วยกันทั้งหมด 3 วิธีคือกำหนดด้วย เมธอด เช่น load วิธีที่สองใช้เมธอดโดยระบุเจาะจงคลาส เช่น [UIView load] และสุดท้ายคือ ฟังก์ชัน เราจะเขียนโปรแกรมขึ้นมาทดลองใช้งาน symbolic breakpoint ง่ายๆดังนี้

Program 20.2
Car.h

Car.m

main.m

เมื่อเขียนโค้ดเสร็จแล้วก็ให้สร้าง symbolic breakpoints โดยเลือกที่เมนู Debug > Breakpoints > Create Symbolic Breakpoints หลังจากนั้นก็จะเจอหน้าต่างดังนี้

car_symbolic

ในโปรแกรมนี้จะกำหนด symbolic breakpoint ด้วยเมธอด info ของคลาส Car ดังนั้นในช่อง Symbol จึงกำหนดเป็น info หรือจะใช้แบบระบุเจาะจงคลาส -[Car info] ก็ได้เช่นเดียวกัน แต่ต้องใช้รูปแบบที่กำหนดคือ -[class method] หรือในกรณีที่เป็นคลาสเมธอดให้ใช้ +[class method] เมื่อให้สั่งให้ทำงาน โปรแกรมก็จะหยุดที่เมธอด info ดังรูป

sym_brek

และถ้าหากสั่งให้ continue เพื่อให้โปรแกรมทำงานต่อไป ก็จะพบว่าโปรแกรมหยุดที่ info อีกรอบ เพราะโปรแกรมเรียกใช้เมธอด info นี้ถึงสามครั้งด้วยกัน การใช้เบรกพ้อนต์ลักษณะนี้มีประโยชน์ในหลายๆกรณี เช่น เมื่อเขียนโปรแกรม iOS หากต้องการวางเบรกพ้อนต์ที่เมธอด viewWillAppear ทุก UIViewController ถ้าโปรแกรมมี view controller จำนวนน้อยคงไม่ใช่ปัญหา แต่ถ้ามี UIViewController จำนวนมากก็จะกลายเป็นปัญหาทันทีเพราะต้องวาง breakpoint หลายที่มาก แต่ในทางกลับกันถ้าเปลี่ยนมาใช้ symbolic breakpoint และกำหนดให้หยุดที่เมธอด viewWillAppear ก็จะใช้เพียงแค่เบรกพ้อนต์เดียวเท่านั้น

Setting Breakpoint Actions and Options

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

Program 20.3
main.m

จากโปรแกรมจะพบว่าค่า x นั้นจะเพิ่มขึ้นไปเรื่อยๆจนถึงจุดหนึ่งค่า x จะกลายเป็นติดลบ เพราะเกิดปัญหา integer overflow หรือค่า x มากเกินกว่าที่ integer จะรองรับได้ หากต้องการจะหาคำตอบว่าลูปทำงานไปกี่รอบค่า x ถึงจะกลายเป็นค่าติดลบ การหาคำตอบนี้คงไม่ได้ยากเย็นอะไรนัก แค่วาง breakpoint และสั่งให้ continue ให้โปรแกรมทำงานไปเรื่อยๆจนกระทั่งค่า x เกิดปัญหา overflow ดังเช่นในรูป ก็จะรู้ว่าลูปทำงานไปกี่รอบ

break_con

แต่อย่างไรก็ตามจากโปรแกรมดังกล่าว กว่าที่จะรู้ว่าเมื่อไหร่ค่า x จะกลายเป็นติดลบ ต้องสั่งให้ continue ถึง 29 ครั้งด้วยกัน คงไม่ใช่เรื่องสนุกที่ต้องนั่งกด continue ไปเรื่อยๆ จนกว่าจะเจอสิ่งที่ต้องการ โชคดีที่สามารถกำหนดให้ breakpoint ให้หยุดตามเงื่อนไขที่ต้องการได้หรือเรียกว่า breakpoint condition
การกำหนด condition ของ breakpoint ทำได้ด้วยการคลิกขวายัง breakpoint ที่ต้องการ แล้วเลือก Edit Breakpoint หลังจากนั้นก็จะพบกับช่องสำหรับกำหนด condition ของ breakpoint ดังรูป

set_break_condition

เมื่อเกิด integer overflow ค่า x จะกลายเป็นติดลบ ดังนั้นเงื่อนของ break point ที่ต้องการให้โปรแกรมหยุด ก็คือ x < 0 หากสั่งให้โปรแกรมทำงานอีกครั้งจะเห็นว่า โปรแกรมจะหยุดที่ i เป็น 29 โดยที่ไม่ต้องกด continue ให้เสียเวลา

set_condition

นอกจากการใช้เงื่อนไขเปรียบเทียบค่าโดยตรง ยังสามารถใช้เมธอดเป็นเงื่อนไขได้เช่นเดียวกัน โดยให้เพิ่ม return type นำหน้าเมธอด เช่น (bool)[myObject isEqualTo:otherObject]  หรือ (int)[array count] == 5

นอกจากการใช้ condition แล้วยังกำหนด action ให้กับ breakpoint ได้อีกด้วย เช่นการเรียกใช้ AppleScript , Log Message หรือสั่งให้เล่นเสียงต่างๆ

action

เราจะลองใช้ Action ง่ายๆ กันสักหนึ่งอย่างคือเรียกใช้ shell command เพื่อให้ Mac OSX พูดบอกเราว่า “Breakpoint has been reached” ด้วยคำสั่ง  say และกำหนด argument เป็น “A breakpoint has been reached”  ดังที่แสดงในรูป

say

เมื่อโปรแกรมหยุดที่ breakpoint ดังกล่าวก็จะได้ยินเสียงพูดตามประโยคที่ได้กำหนดไว้ นั่นเอง

Release and Debug

หลังจากที่ได้แก้ไขโปรแกรมจนแน่ใจว่า ไม่มีบั๊กแล้ว ก็ถึงเวลาที่จะนำโปรแกรมไปใช้งานจริง แต่ก่อนที่จะนำไปให้ผู้ใช้ได้ใช้งานจริงๆนั้น จำเป็นต้องปรับค่าของ compiler ให้คอมไพล์โปรแกรมให้มีประสิทธิภาพมากที่สุด ทำงานได้เร็วที่สุด และมีขนาดเล็กที่สุด นั่นก็คือ optimize โปรแกรมและนำ debug symbol หรือโค้ดส่วนที่ใช้ในการ debug ออกไป และเรียกว่า release นั่นเอง การปรับโปรแกรมให้เป็น release สามารถทำได้โดยการปรับ scheme ที่เมนู Product > Scheme > Edit Scheme จากนั้นก็จะปรากฎหน้าต่างการปรับแต่ง scheme

scheme

ตอนนี้ Scheme Run นั้นมี Build Configuration เป็น Debug เราอาจจะสร้าง Scheme ขึ้นมาใหม่และเลือก Build Configuration ให้เป็น release ก็ได้ หรือจะปรับโดยตรงที่ scheme ที่ต้องการก็ได้เช่นกัน นอกจากการปรับ scheme แล้วยังสามารถที่จะสร้าง Application แบบ release ได้ด้วยการเลือกเมนู Product > Archive จากนั้น XCode จะคอมไพล์โปรแกรมเป็น release และจะเก็บไฟล์ไว้ให้เรา

archive

จากนั้นก็เลือก Distribute เมื่อต้องการจะแจกจ่ายโปรแกรมให้ผู้ใช้งาน

 

Leave a Reply