ผลต่างระหว่างรุ่นของ "Afgu/unit testing 1"
Jittat (คุย | มีส่วนร่วม) (หน้าที่ถูกสร้างด้วย 'เราใช้หัดเขียน unit test บน java script ซึ่งเป็นภาษาที่ทุกคนน...') |
Jittat (คุย | มีส่วนร่วม) |
||
(ไม่แสดง 55 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
แถว 1: | แถว 1: | ||
+ | : ''หน้านีเป็นส่วนหนึ่งของชุดแบบฝึกหัด [[afgu|Agile from the ground up]] | ||
+ | |||
เราใช้หัดเขียน unit test บน java script ซึ่งเป็นภาษาที่ทุกคนน่าจะสามารถเรียกให้ทำงานได้ ในครั้งแรกเราจะเน้นให้เข้าใจว่า unit test คืออะไร และสามารถเขียน unit test แบบทั่วไปได้ ในครั้งถัด ๆ ไปเราจะศึกษาเทคนิคเพิ่มเติมเช่นการทำ isolation รวมไปถึงการเขียน unit test ที่ดี | เราใช้หัดเขียน unit test บน java script ซึ่งเป็นภาษาที่ทุกคนน่าจะสามารถเรียกให้ทำงานได้ ในครั้งแรกเราจะเน้นให้เข้าใจว่า unit test คืออะไร และสามารถเขียน unit test แบบทั่วไปได้ ในครั้งถัด ๆ ไปเราจะศึกษาเทคนิคเพิ่มเติมเช่นการทำ isolation รวมไปถึงการเขียน unit test ที่ดี | ||
+ | |||
+ | เราสามารถทำ unit testing ได้โดยไม่ต้องใช้ framework ใด ๆ เลยก็ได้ แต่ในที่นี้เราจะใช้ [http://visionmedia.github.io/mocha/ mocha] เป็น framework <tt>mocha</tt> รองรับไลบรารีการ assert/expect ได้หลายแบบ เราเลือกใช้ [http://chaijs.com/ chai] นอกจากนี้ <tt>mocha</tt> ยังต้องการใช้ [http://jquery.com/ jquery] ในการแสดงผล เราจึงต้องเรียก jquery ด้วย | ||
+ | |||
+ | ไลบรารีที่ใช้: | ||
+ | |||
+ | * [http://visionmedia.github.io/mocha/ mocha] | ||
+ | * [http://chaijs.com/ Chai Assertion Library] | ||
+ | * [http://jquery.com/ jQuery] | ||
+ | |||
+ | ที่เราเลือกใช้ <tt>mocha</tt> และ <tt>Chai</tt> นั้นเป็นตามรสนิยมผู้สอน ในการใช้งานจริง แนะนำให้เลือกไลบรารี/เฟรมเวิร์คที่ชอบตามสะดวก | ||
+ | |||
+ | == โครงสร้างไดเร็กทอรี == | ||
+ | |||
+ | ในแต่ละตัวอย่างและแบบฝึกหัดที่เราจะเขียน เราจะใช้โครงสร้างไดเร็กทอรีดังนี้ | ||
+ | |||
+ | - project/ | ||
+ | - *.js (ไฟล์ js ของ project) | ||
+ | - test/ | ||
+ | - index-test.html | ||
+ | - test.js (เก็บโค้ดสำหรับ test) | ||
+ | - lib/ | ||
+ | - mocha.js | ||
+ | - mocha.css | ||
+ | - chai.js | ||
+ | - jquery.js | ||
+ | |||
+ | สามารถดาวน์โหลด template ดังกล่าวได้: [http://theory.cpe.ku.ac.th/~jittat/afgu/unittest1/project.tgz project.tgz], [http://theory.cpe.ku.ac.th/~jittat/afgu/unittest1/project.zip project.zip] และเปลี่ยนชื่อไดเร็กทอรีตามความเหมาะสม | ||
+ | |||
+ | == ตัวอย่าง == | ||
+ | เราต้องการเขียนฟังก์ชัน | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | function cirIntersect(x1, y1, r1, x2, y2, r2) { | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ที่รับข้อมูล | ||
+ | |||
+ | * วงกลมวงแรก ที่มีจุดศูนย์กลางที่ตำแหน่ง (x1,y1) รัศมี r1 และ | ||
+ | * วงกลมวงที่สอง ที่มีจุดศูนย์กลางที่ตำแหน่ง (x2,y2) รัศมี r2 | ||
+ | |||
+ | จากนั้นคืนค่า | ||
+ | |||
+ | * true ถ้าวงกลมทั้งสองวงมีเส้นรอบวงที่ตัดกันหรือสัมผัสกัน (ถ้าวงกลมที่ซ้อนกันโดยที่วงเล็กอยู่ภายในวงใหญ่โดยไม่สัมผัสกันเลยจะไม่นับ) | ||
+ | |||
+ | สมมติว่ามีคนเขียนฟังก์ชันดังกล่าวมาให้เรา เราจะ "ทดสอบ" อะไรบ้าง ที่ทำให้เราเชื่อได้ว่าฟังก์ชันดังกล่าวทำงานได้ถูกต้อง? | ||
+ | |||
+ | === กรณีทดสอบตัวอย่าง === | ||
+ | |||
+ | ตัวอย่างหนึ่งที่เราทดสอบได้คือกรณีที่วงกลมสองวงห่างกันมาก ๆ จนเส้นรอบวงไม่ทับกัน (เราควรวาดรูปประกอบด้วย) | ||
+ | |||
+ | {| class="wikitable" | ||
+ | !กรณี !! กรณีที่ทดสอบ !! x1 !! y1 !! r1 !! x2 !! y2 !! r2 !! return | ||
+ | |- | ||
+ | | 1 || วงกลมห่างกัน || 0 || 0 || 10 || 100 || 0 || 10 || false | ||
+ | |} | ||
+ | |||
+ | ใน <tt>test.js</tt> เราจะอธิบายกรณีทดสอบนี้ได้ดังนี้ | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('cirIntersect', function(){ | ||
+ | |||
+ | it('should return false when two circles are far apart', function(){ | ||
+ | assert(cirIntersect(0,0,10,100,0,10) == false); | ||
+ | }); | ||
+ | |||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ในส่วนด้านบนเราระบุ: | ||
+ | |||
+ | * describe ระบุว่าจะอธิบายอะไร | ||
+ | * it (should) ระบุว่าสิ่งที่จะอธิบายจะต้องทำอะไรได้ | ||
+ | * function ที่ระบุใน it นั้นเป็นโค้ดสำหรับทดสอบว่าสิ่งที่จะอธิบาย ทำสิ่งที่ระบุได้ | ||
+ | * assert เป็น "เงื่อนไข" ที่เราจะทดสอบ | ||
+ | |||
+ | === เขียนโค้ด === | ||
+ | |||
+ | สังเกตว่าเรายังไม่ได้เขียนโค้ดอะไรเลยของฟังก์ชัน <tt>cirIntersect</tt> เราลองใช้ browser เปิด <tt>index-test.html</tt> เราจะเห็นว่าโปรแกรมของเรายังไม่ผ่านการทดสอบดังกล่าว | ||
+ | |||
+ | จากกรณีทดสอบตัวอย่าง เราจะแก้โค้ดของฟังก์ชันเพื่อให้ทำงานให้ผ่าน (ถ้าเราต้องการทำตามหลักการ เราจะเขียนโค้ดให้ง่ายที่สุดให้โปรแกรมทำงานผ่านข้อมูลทดสอบนี้) | ||
+ | |||
+ | เราอาจจะเขียนฟังก์ชันดังกล่าวดังนี้ | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | function cirIntersect(x1, y1, r1, x2, y2, r2) { | ||
+ | return false; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | แต่เรามองแว่บเดียวก็เห็นแล้วว่าฟังก์ชันดังกล่าวทำงานไม่ถูกแน่ ๆ แต่ถ้ากรณีทดสอบเรามีแค่ข้อเดียวดังข้างต้น เราก็ไม่สามารถบอกได้ว่าโค้ดขำ ๆ ข้างบนนี้ทำงานผิดพลาด | ||
+ | |||
+ | === กรณีที่ตัดกันแบบง่าย ๆ === | ||
+ | |||
+ | เราจะเพิ่มกรณีง่าย ๆ ที่เรานึกออก โดยให้วงกลมใหญ่หน่อยและตัดกัน | ||
+ | |||
+ | {| class="wikitable" | ||
+ | !กรณี !! กรณีที่ทดสอบ !! x1 !! y1 !! r1 !! x2 !! y2 !! r2 !! return | ||
+ | |- | ||
+ | | 2 || วงกลมตัดกันบนแกน x || 0 || 0 || 10 || 15 || 0 || 10 || true | ||
+ | |} | ||
+ | |||
+ | และเพิ่มโค้ดใน <tt>test.js</tt> เป็น | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('cirIntersect', function(){ | ||
+ | |||
+ | it('should return false when two circles are far apart', function(){ | ||
+ | assert(cirIntersect(0,0,10,100,0,10)==false); | ||
+ | }); | ||
+ | |||
+ | it('should return true when two circles on the x-axis intersect', function(){ | ||
+ | assert(cirIntersect(0,0,10,15,0,10)==true); | ||
+ | }); | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | เราสามารถแก้โค้ดให้ทำงานผ่านได้ง่าย ๆ โดยเปรียบเทียบระยะทางแกน x ได้ดังด้านล่าง | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | function cirIntersect(x1, y1, r1, x2, y2, r2) { | ||
+ | return Math.abs(x2 - x1) < (r1+r2); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | โค้ดถูกต้องหรือยัง? แน่นอนว่ายัง... | ||
+ | |||
+ | ขั้นตอนที่เราทำนี้ ถ้าเราคิดได้เร็วหน่อย เราอาจจะไม่ต้องทำซ้ำ ๆ หลายรอบแบบนี้ก็ได้ แต่เพื่อเป็นการฝึกหัด เราจะทยอยแก้แบบง่าย ๆ แบบนี้ไปเรื่อย ๆ ก่อน | ||
+ | |||
+ | === เพิ่มกรณีทดสอบ === | ||
+ | |||
+ | โค้ดเรายังทำงานไม่ถูก แต่เราจะทำอย่างไรให้ทำงานถูกต้อง เราจะใช้กรณีทดสอบบังคับโค้ดให้ทำงานให้ถูกต้อง | ||
+ | |||
+ | ลองเขียนกรณีทดสอบเพิ่ม โดยระบุในส่วน <tt>it('.....',function(){});</tt> ดังตัวอย่าง: | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('cirIntersect', function(){ | ||
+ | // ... | ||
+ | it('should blah blah', function(){ | ||
+ | // ... | ||
+ | }); | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | และสลับไปเขียนโค้ดให้ทำงานผ่าน ถ้าโค้ดทำงานผ่านอยู่แล้วก็ไม่ต้องแก้ไขอะไร | ||
+ | |||
+ | == แบบฝึกหัด 0: การแปลงวันที่ == | ||
+ | |||
+ | ในการพัฒนาโปรแกรมหลาย ๆ แบบเราต้องการรับ "วันที่" จากผู้ใช้ อย่างไรก็ตามรูปแบบในการป้อนวันที่มีหลากหลาย และไม่ใช่ว่าการกางปฏิทินให้ผู้ใช้กดจะเป็นทางเลือกที่ดีเสมอไป ใน JavaScript มีคลาส [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FDate Date] แต่เมทอดสำหรับแปลงวันที่จากสตริงนั้นค่อนข้างมีขอบเขตที่จำกัด และถ้าสตริงเป็นวันที่ภาษาไทย เช่น <tt>11 พ.ย. 56</tt> เมทอดนี้คงจะทำงานไม่ได้แน่ ๆ เราต้องการเขียนฟังก์ชัน | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | function toDate(str) {} | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ที่รับสตริงและคืนค่าเป็น <tt>Date</tt> | ||
+ | |||
+ | === ตัวอย่างแรก === | ||
+ | |||
+ | อย่างน้อยฟังก์ชันของเราควรจะแปลสตริงแบบมาตรฐาน (ที่ <tt>Date</tt> แปลงได้) เป็นวันที่ให้ได้ก่อน เราเขียนอธิบายใน <tt>test.js</tt> ดังนี้ | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('toDate', function(){ | ||
+ | it('should convert date in YYYY-MM-DD format', function(){ | ||
+ | var d = toDate('2013-11-12'); | ||
+ | var correctDate = new Date(2013,10,12); | ||
+ | assert(d == correctDate); | ||
+ | }); | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | หมายเหตุ: Date รับพารามิเตอร์ month โดยเริ่มที่ 0 ดังนั้นเดือนพฤศจิกายน จะมีค่าเป็น 10 (ไม่ใช่ 11) | ||
+ | |||
+ | จากนั้นไปแก้ฟังก์ชัน <tt>toDate</tt> (ใน project.js หรือในไฟล์อื่น) เป็น | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | function toDate(str) { | ||
+ | return new Date(str); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ทดลองรันเราจะพบว่า test เรายังไม่ผ่าน ทั้งนี้เนื่องมาจากการเปรียบเทียบวันที่โดยเครื่องหมาย == นั้น ทำไม่ได้ เราต้องแก้โค้ดใน test.js ใหม่ ให้เป็นดังนี้ | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('toDate', function(){ | ||
+ | it('should convert date in YYYY-MM-DD format', function(){ | ||
+ | var d = toDate('2013-11-12'); | ||
+ | var correctDate = new Date(2013,10,12); | ||
+ | assert.equal(d.toDateString(),correctDate.toDateString()); | ||
+ | }); | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | '''หมายเหตุ:''' เราเรียก <tt>toDateString()</tt> เพื่อป้องกันปัญหาพวก time-zone ด้วย ในการพัฒนาโปรแกรมจริง ๆ เราจะต้องคำนึงถึงปัญหาดังกล่าวมากกว่าในตัวอย่างง่าย ๆ ที่เราใช้ฝึกหัดนี้ | ||
+ | |||
+ | === ตัวอย่างที่สอง === | ||
+ | เราลองเพิ่มรูปแบบของวันที่ภาษาอังกฤษที่ซับซ้อนเข้าไป | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('toDate', function(){ | ||
+ | it('should convert date in YYYY-MM-DD format', function(){ | ||
+ | var d = toDate('2013-11-12'); | ||
+ | var correctDate = new Date(2013,10,12); | ||
+ | assert.equal(d.toDateString(), correctDate.toDateString()); | ||
+ | }); | ||
+ | |||
+ | it('should convert date in DD Month YYY format', function(){ | ||
+ | var d = toDate('12 Nov 2013'); | ||
+ | var correctDate = new Date(2013,10,12); | ||
+ | assert.equal(d.toDateString(), correctDate.toDateString()); | ||
+ | }); | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ปรากฏว่าโค้ดเดิมของเราทำงานได้ ดังนั้นเราจึงไม่ต้องแก้โค้ดแต่อย่างใด | ||
+ | |||
+ | === ความซ้ำซาก: หลักการ DRY (Don't Repeat Yourself) === | ||
+ | ในโค้ดข้างต้น ถ้าเราสังเกตดี ๆ จะมีความไม่น่าพิศมัยหลายอย่าง เช่น | ||
+ | |||
+ | * เราพบว่าเรามีการสร้าง <tt>correctDate</tt> หลายครั้ง | ||
+ | * ในการตรวจสอบวันที่ เรายังมีโค้ดสำหรับแปลง <tt>toDateString</tt> ปรากฏอยู่ทั่วไป | ||
+ | * ชื่อตัวแปร <tt>correctDate</tt> ไม่ได้สื่อความหมายอะไรเท่าใด (คือ จรึง ๆ มันก็พอจะสื่อความหมายอยู่ "บ้าง" แต่ในที่นี้เราดูชื่อตัวแปรแล้วไม่ทราบว่าวันที่ต้องเป็นเท่าใดถึงจะถูกต้อง) | ||
+ | |||
+ | สิ่งที่ไม่น่าพิศมัยสองข้อแรกนั้น เกิดจากการที่เรามีโค้ดสำหรับแนวคิดหนึ่งปรากฏอยู่หลายที่ (Repeat Yourself) ทำให้ถ้าเรา (หรือคนอื่น) ต้องปรับแก้ เราต้องตามไปแก้ทุกที่ ซึ่งมีความเสี่ยงอย่างมากที่จะแก้ขาดหรือแก้เกิน | ||
+ | |||
+ | หลักการหนึ่งที่อธิบายเกี่ยวกับปรากฏการณ์นี้เรียกว่า | ||
+ | |||
+ | DRY: Don't Repeat Yourself | ||
+ | |||
+ | ในความเห็นผม ถ้าคุณจะยึดถือหลักการเดียวในการพัฒนาซอฟต์แวร์ ก็ควรจะเป็นหลักการนี้ | ||
+ | |||
+ | เราจะปรับโค้ดใน <tt>test.js</tt> ใหม่ เพื่อปรับแก้ปัญหาดังกล่าว ดังนี้ | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('toDate', function(){ | ||
+ | |||
+ | var nov12_2013 = new Date(2013,10,12); | ||
+ | |||
+ | it('should convert date in YYYY-MM-DD format', function(){ | ||
+ | var d = toDate('2013-11-12'); | ||
+ | assertDateEqual(d, nov12_2013); | ||
+ | }); | ||
+ | |||
+ | it('should convert date in DD Month YYY format', function(){ | ||
+ | var d = toDate('12 Nov 2013'); | ||
+ | assertDateEqual(d, nov12_2013); | ||
+ | }); | ||
+ | |||
+ | function assertDateEqual(dateActual, dateExpected, message){ | ||
+ | assert.equal(dateActual.toDateString(), | ||
+ | dateExpected.toDateString(), | ||
+ | message); | ||
+ | } | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | '''หมายเหตุ:''' ใน javascript ถ้าเราประกาศฟังก์ชันที่ตำแหน่งใด scope ฟังก์ชันนั้นจะถูกขยายไปจนถึงขอบบนของ scope นัั้น นั่นคือสาเหตุว่าทำไมเราสามารถเรียกใช้ assertDateEqual ได้ตั้งแต่ต้นโปรแกรม ทั้ง ๆ ที่เราประกาศฟังก์ชันนี้ไว้ตอนท้าย | ||
+ | |||
+ | === วันที่ภาษาไทย === | ||
+ | เราจะเพิ่มอีกตัวอย่างที่เป็นวันที่ภาษาไทย | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('toDate', function(){ | ||
+ | // ... | ||
+ | |||
+ | it('should convert date in วว เดือน พศพศ format', function(){ | ||
+ | var d = toDate('12 พฤศจิกายน 2556'); | ||
+ | assertDateEqual(d, nov12_2013); | ||
+ | }); | ||
+ | |||
+ | // ... | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | แล้วก็กลับไปแก้ <tt>toDate</tt> ให้ผ่าน... | ||
+ | |||
+ | วนเวียนเป็นวงรอบไปเรื่อย ๆ | ||
+ | |||
+ | == แบบฝึกหัด 1a: วงกลมตัดกับสี่เหลี่ยม == | ||
+ | เขียนฟังก์ชั่น | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | function cirRectIntersect(xc,yc,rc,x1,y1,x2,y2) {} | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ที่รับข้อมูลของวงกลมที่มีจุดศูนย์กลางที่ (xc,yc) มีรัศมี rc กับสี่เหลี่ยมที่มีมุมบนซ้ายที่ (x1,y1) มุมล่างขวาที่ (x2,y2) จากนั้นคืนค่า true ถ้าเส้นรอบรูปของวงกลมกับสี่เหลี่ยมสัมผัสกัน | ||
+ | |||
+ | == แบบฝึกหัด 1b: สถานะเกม OX == | ||
+ | เขียนฟังก์ชัน | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | function OXStatus(board) {} | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ที่รับสถานะของตารางเกม O-X ในพารามิเตอร์ <tt>board</tt> ที่อยู่ในรูปของ [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FArray Array] ที่ประกอบด้วยสตริง 3 ตัว แต่ละตัวประกอบด้วยอักษร 'X', 'O', '.' ความยาว 3 ตัวอักษร | ||
+ | |||
+ | ตัวอย่างการเรียกใช้เป็นดังด้านล่าง | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('OXStatus', function(){ | ||
+ | it('should tell X wins with the center vertical column', function(){ | ||
+ | assert(OXStatus(['OX.', | ||
+ | 'OX.', | ||
+ | '.XO'])=='X wins'); | ||
+ | }); | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ค่าที่คืนเป็นสตริง โดยในการเขียน คุณจะใช้ค่าอะไรแทนสถานะอย่างไรก็ได้ตามสะดวก อย่างไรก็ตามฟังก์ชันควรแยกแยะกรณีที่ X ชนะ, O ชนะ, เสมอ, เกมยังไม่จบ, หรือเกมเป็นไปไม่ได้ ได้ | ||
+ | |||
+ | == แบบฝึกหัด 2: วัตถุและสถานะ - Tennis Game == | ||
+ | : ''จาก [http://www.codingdojo.org/cgi-bin/wiki.pl?KataTennis KataTennis ที่ Coding Dojo]'' | ||
+ | |||
+ | ด้านล่างเป็นโครงของคลาส | ||
+ | |||
+ | <syntaxhighlight lang="javascript"> | ||
+ | function TennisGame() { | ||
+ | this.scoreA = 0; | ||
+ | this.scoreB = 0; | ||
+ | } | ||
+ | |||
+ | TennisGame.prototype.getStatus = function() { | ||
+ | return 'a:0-b:0'; | ||
+ | }; | ||
+ | TennisGame.prototype.aScore = function() {}; | ||
+ | TennisGame.prototype.bScore = function() {}; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ด้านล่างเป็นตัวอย่างของบาง test | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | describe('TennisGame', function(){ | ||
+ | var tennisGame = null; | ||
+ | |||
+ | beforeEach(function(){ | ||
+ | tennisGame = new TennisGame(); | ||
+ | }); | ||
+ | |||
+ | it('should return status when the game starts', function(){ | ||
+ | assert.equal(tennisGame.getStatus(),'a:0-b:0'); | ||
+ | }); | ||
+ | |||
+ | it('should return status when A score one point', function(){ | ||
+ | tennisGame.aScore(); | ||
+ | assert.equal(tennisGame.getStatus(),'a:15-b:0'); | ||
+ | }); | ||
+ | }); | ||
+ | </syntaxhighlight> |
รุ่นแก้ไขปัจจุบันเมื่อ 17:28, 11 พฤศจิกายน 2556
- หน้านีเป็นส่วนหนึ่งของชุดแบบฝึกหัด Agile from the ground up
เราใช้หัดเขียน unit test บน java script ซึ่งเป็นภาษาที่ทุกคนน่าจะสามารถเรียกให้ทำงานได้ ในครั้งแรกเราจะเน้นให้เข้าใจว่า unit test คืออะไร และสามารถเขียน unit test แบบทั่วไปได้ ในครั้งถัด ๆ ไปเราจะศึกษาเทคนิคเพิ่มเติมเช่นการทำ isolation รวมไปถึงการเขียน unit test ที่ดี
เราสามารถทำ unit testing ได้โดยไม่ต้องใช้ framework ใด ๆ เลยก็ได้ แต่ในที่นี้เราจะใช้ mocha เป็น framework mocha รองรับไลบรารีการ assert/expect ได้หลายแบบ เราเลือกใช้ chai นอกจากนี้ mocha ยังต้องการใช้ jquery ในการแสดงผล เราจึงต้องเรียก jquery ด้วย
ไลบรารีที่ใช้:
ที่เราเลือกใช้ mocha และ Chai นั้นเป็นตามรสนิยมผู้สอน ในการใช้งานจริง แนะนำให้เลือกไลบรารี/เฟรมเวิร์คที่ชอบตามสะดวก
เนื้อหา
โครงสร้างไดเร็กทอรี
ในแต่ละตัวอย่างและแบบฝึกหัดที่เราจะเขียน เราจะใช้โครงสร้างไดเร็กทอรีดังนี้
- project/ - *.js (ไฟล์ js ของ project) - test/ - index-test.html - test.js (เก็บโค้ดสำหรับ test) - lib/ - mocha.js - mocha.css - chai.js - jquery.js
สามารถดาวน์โหลด template ดังกล่าวได้: project.tgz, project.zip และเปลี่ยนชื่อไดเร็กทอรีตามความเหมาะสม
ตัวอย่าง
เราต้องการเขียนฟังก์ชัน
function cirIntersect(x1, y1, r1, x2, y2, r2) {
}
ที่รับข้อมูล
- วงกลมวงแรก ที่มีจุดศูนย์กลางที่ตำแหน่ง (x1,y1) รัศมี r1 และ
- วงกลมวงที่สอง ที่มีจุดศูนย์กลางที่ตำแหน่ง (x2,y2) รัศมี r2
จากนั้นคืนค่า
- true ถ้าวงกลมทั้งสองวงมีเส้นรอบวงที่ตัดกันหรือสัมผัสกัน (ถ้าวงกลมที่ซ้อนกันโดยที่วงเล็กอยู่ภายในวงใหญ่โดยไม่สัมผัสกันเลยจะไม่นับ)
สมมติว่ามีคนเขียนฟังก์ชันดังกล่าวมาให้เรา เราจะ "ทดสอบ" อะไรบ้าง ที่ทำให้เราเชื่อได้ว่าฟังก์ชันดังกล่าวทำงานได้ถูกต้อง?
กรณีทดสอบตัวอย่าง
ตัวอย่างหนึ่งที่เราทดสอบได้คือกรณีที่วงกลมสองวงห่างกันมาก ๆ จนเส้นรอบวงไม่ทับกัน (เราควรวาดรูปประกอบด้วย)
กรณี | กรณีที่ทดสอบ | x1 | y1 | r1 | x2 | y2 | r2 | return |
---|---|---|---|---|---|---|---|---|
1 | วงกลมห่างกัน | 0 | 0 | 10 | 100 | 0 | 10 | false |
ใน test.js เราจะอธิบายกรณีทดสอบนี้ได้ดังนี้
describe('cirIntersect', function(){
it('should return false when two circles are far apart', function(){
assert(cirIntersect(0,0,10,100,0,10) == false);
});
});
ในส่วนด้านบนเราระบุ:
- describe ระบุว่าจะอธิบายอะไร
- it (should) ระบุว่าสิ่งที่จะอธิบายจะต้องทำอะไรได้
- function ที่ระบุใน it นั้นเป็นโค้ดสำหรับทดสอบว่าสิ่งที่จะอธิบาย ทำสิ่งที่ระบุได้
- assert เป็น "เงื่อนไข" ที่เราจะทดสอบ
เขียนโค้ด
สังเกตว่าเรายังไม่ได้เขียนโค้ดอะไรเลยของฟังก์ชัน cirIntersect เราลองใช้ browser เปิด index-test.html เราจะเห็นว่าโปรแกรมของเรายังไม่ผ่านการทดสอบดังกล่าว
จากกรณีทดสอบตัวอย่าง เราจะแก้โค้ดของฟังก์ชันเพื่อให้ทำงานให้ผ่าน (ถ้าเราต้องการทำตามหลักการ เราจะเขียนโค้ดให้ง่ายที่สุดให้โปรแกรมทำงานผ่านข้อมูลทดสอบนี้)
เราอาจจะเขียนฟังก์ชันดังกล่าวดังนี้
function cirIntersect(x1, y1, r1, x2, y2, r2) {
return false;
}
แต่เรามองแว่บเดียวก็เห็นแล้วว่าฟังก์ชันดังกล่าวทำงานไม่ถูกแน่ ๆ แต่ถ้ากรณีทดสอบเรามีแค่ข้อเดียวดังข้างต้น เราก็ไม่สามารถบอกได้ว่าโค้ดขำ ๆ ข้างบนนี้ทำงานผิดพลาด
กรณีที่ตัดกันแบบง่าย ๆ
เราจะเพิ่มกรณีง่าย ๆ ที่เรานึกออก โดยให้วงกลมใหญ่หน่อยและตัดกัน
กรณี | กรณีที่ทดสอบ | x1 | y1 | r1 | x2 | y2 | r2 | return |
---|---|---|---|---|---|---|---|---|
2 | วงกลมตัดกันบนแกน x | 0 | 0 | 10 | 15 | 0 | 10 | true |
และเพิ่มโค้ดใน test.js เป็น
describe('cirIntersect', function(){
it('should return false when two circles are far apart', function(){
assert(cirIntersect(0,0,10,100,0,10)==false);
});
it('should return true when two circles on the x-axis intersect', function(){
assert(cirIntersect(0,0,10,15,0,10)==true);
});
});
เราสามารถแก้โค้ดให้ทำงานผ่านได้ง่าย ๆ โดยเปรียบเทียบระยะทางแกน x ได้ดังด้านล่าง
function cirIntersect(x1, y1, r1, x2, y2, r2) {
return Math.abs(x2 - x1) < (r1+r2);
}
โค้ดถูกต้องหรือยัง? แน่นอนว่ายัง...
ขั้นตอนที่เราทำนี้ ถ้าเราคิดได้เร็วหน่อย เราอาจจะไม่ต้องทำซ้ำ ๆ หลายรอบแบบนี้ก็ได้ แต่เพื่อเป็นการฝึกหัด เราจะทยอยแก้แบบง่าย ๆ แบบนี้ไปเรื่อย ๆ ก่อน
เพิ่มกรณีทดสอบ
โค้ดเรายังทำงานไม่ถูก แต่เราจะทำอย่างไรให้ทำงานถูกต้อง เราจะใช้กรณีทดสอบบังคับโค้ดให้ทำงานให้ถูกต้อง
ลองเขียนกรณีทดสอบเพิ่ม โดยระบุในส่วน it('.....',function(){}); ดังตัวอย่าง:
describe('cirIntersect', function(){
// ...
it('should blah blah', function(){
// ...
});
});
และสลับไปเขียนโค้ดให้ทำงานผ่าน ถ้าโค้ดทำงานผ่านอยู่แล้วก็ไม่ต้องแก้ไขอะไร
แบบฝึกหัด 0: การแปลงวันที่
ในการพัฒนาโปรแกรมหลาย ๆ แบบเราต้องการรับ "วันที่" จากผู้ใช้ อย่างไรก็ตามรูปแบบในการป้อนวันที่มีหลากหลาย และไม่ใช่ว่าการกางปฏิทินให้ผู้ใช้กดจะเป็นทางเลือกที่ดีเสมอไป ใน JavaScript มีคลาส Date แต่เมทอดสำหรับแปลงวันที่จากสตริงนั้นค่อนข้างมีขอบเขตที่จำกัด และถ้าสตริงเป็นวันที่ภาษาไทย เช่น 11 พ.ย. 56 เมทอดนี้คงจะทำงานไม่ได้แน่ ๆ เราต้องการเขียนฟังก์ชัน
function toDate(str) {}
ที่รับสตริงและคืนค่าเป็น Date
ตัวอย่างแรก
อย่างน้อยฟังก์ชันของเราควรจะแปลสตริงแบบมาตรฐาน (ที่ Date แปลงได้) เป็นวันที่ให้ได้ก่อน เราเขียนอธิบายใน test.js ดังนี้
describe('toDate', function(){
it('should convert date in YYYY-MM-DD format', function(){
var d = toDate('2013-11-12');
var correctDate = new Date(2013,10,12);
assert(d == correctDate);
});
});
หมายเหตุ: Date รับพารามิเตอร์ month โดยเริ่มที่ 0 ดังนั้นเดือนพฤศจิกายน จะมีค่าเป็น 10 (ไม่ใช่ 11)
จากนั้นไปแก้ฟังก์ชัน toDate (ใน project.js หรือในไฟล์อื่น) เป็น
function toDate(str) {
return new Date(str);
}
ทดลองรันเราจะพบว่า test เรายังไม่ผ่าน ทั้งนี้เนื่องมาจากการเปรียบเทียบวันที่โดยเครื่องหมาย == นั้น ทำไม่ได้ เราต้องแก้โค้ดใน test.js ใหม่ ให้เป็นดังนี้
describe('toDate', function(){
it('should convert date in YYYY-MM-DD format', function(){
var d = toDate('2013-11-12');
var correctDate = new Date(2013,10,12);
assert.equal(d.toDateString(),correctDate.toDateString());
});
});
หมายเหตุ: เราเรียก toDateString() เพื่อป้องกันปัญหาพวก time-zone ด้วย ในการพัฒนาโปรแกรมจริง ๆ เราจะต้องคำนึงถึงปัญหาดังกล่าวมากกว่าในตัวอย่างง่าย ๆ ที่เราใช้ฝึกหัดนี้
ตัวอย่างที่สอง
เราลองเพิ่มรูปแบบของวันที่ภาษาอังกฤษที่ซับซ้อนเข้าไป
describe('toDate', function(){
it('should convert date in YYYY-MM-DD format', function(){
var d = toDate('2013-11-12');
var correctDate = new Date(2013,10,12);
assert.equal(d.toDateString(), correctDate.toDateString());
});
it('should convert date in DD Month YYY format', function(){
var d = toDate('12 Nov 2013');
var correctDate = new Date(2013,10,12);
assert.equal(d.toDateString(), correctDate.toDateString());
});
});
ปรากฏว่าโค้ดเดิมของเราทำงานได้ ดังนั้นเราจึงไม่ต้องแก้โค้ดแต่อย่างใด
ความซ้ำซาก: หลักการ DRY (Don't Repeat Yourself)
ในโค้ดข้างต้น ถ้าเราสังเกตดี ๆ จะมีความไม่น่าพิศมัยหลายอย่าง เช่น
- เราพบว่าเรามีการสร้าง correctDate หลายครั้ง
- ในการตรวจสอบวันที่ เรายังมีโค้ดสำหรับแปลง toDateString ปรากฏอยู่ทั่วไป
- ชื่อตัวแปร correctDate ไม่ได้สื่อความหมายอะไรเท่าใด (คือ จรึง ๆ มันก็พอจะสื่อความหมายอยู่ "บ้าง" แต่ในที่นี้เราดูชื่อตัวแปรแล้วไม่ทราบว่าวันที่ต้องเป็นเท่าใดถึงจะถูกต้อง)
สิ่งที่ไม่น่าพิศมัยสองข้อแรกนั้น เกิดจากการที่เรามีโค้ดสำหรับแนวคิดหนึ่งปรากฏอยู่หลายที่ (Repeat Yourself) ทำให้ถ้าเรา (หรือคนอื่น) ต้องปรับแก้ เราต้องตามไปแก้ทุกที่ ซึ่งมีความเสี่ยงอย่างมากที่จะแก้ขาดหรือแก้เกิน
หลักการหนึ่งที่อธิบายเกี่ยวกับปรากฏการณ์นี้เรียกว่า
DRY: Don't Repeat Yourself
ในความเห็นผม ถ้าคุณจะยึดถือหลักการเดียวในการพัฒนาซอฟต์แวร์ ก็ควรจะเป็นหลักการนี้
เราจะปรับโค้ดใน test.js ใหม่ เพื่อปรับแก้ปัญหาดังกล่าว ดังนี้
describe('toDate', function(){
var nov12_2013 = new Date(2013,10,12);
it('should convert date in YYYY-MM-DD format', function(){
var d = toDate('2013-11-12');
assertDateEqual(d, nov12_2013);
});
it('should convert date in DD Month YYY format', function(){
var d = toDate('12 Nov 2013');
assertDateEqual(d, nov12_2013);
});
function assertDateEqual(dateActual, dateExpected, message){
assert.equal(dateActual.toDateString(),
dateExpected.toDateString(),
message);
}
});
หมายเหตุ: ใน javascript ถ้าเราประกาศฟังก์ชันที่ตำแหน่งใด scope ฟังก์ชันนั้นจะถูกขยายไปจนถึงขอบบนของ scope นัั้น นั่นคือสาเหตุว่าทำไมเราสามารถเรียกใช้ assertDateEqual ได้ตั้งแต่ต้นโปรแกรม ทั้ง ๆ ที่เราประกาศฟังก์ชันนี้ไว้ตอนท้าย
วันที่ภาษาไทย
เราจะเพิ่มอีกตัวอย่างที่เป็นวันที่ภาษาไทย
describe('toDate', function(){
// ...
it('should convert date in วว เดือน พศพศ format', function(){
var d = toDate('12 พฤศจิกายน 2556');
assertDateEqual(d, nov12_2013);
});
// ...
});
แล้วก็กลับไปแก้ toDate ให้ผ่าน...
วนเวียนเป็นวงรอบไปเรื่อย ๆ
แบบฝึกหัด 1a: วงกลมตัดกับสี่เหลี่ยม
เขียนฟังก์ชั่น
function cirRectIntersect(xc,yc,rc,x1,y1,x2,y2) {}
ที่รับข้อมูลของวงกลมที่มีจุดศูนย์กลางที่ (xc,yc) มีรัศมี rc กับสี่เหลี่ยมที่มีมุมบนซ้ายที่ (x1,y1) มุมล่างขวาที่ (x2,y2) จากนั้นคืนค่า true ถ้าเส้นรอบรูปของวงกลมกับสี่เหลี่ยมสัมผัสกัน
แบบฝึกหัด 1b: สถานะเกม OX
เขียนฟังก์ชัน
function OXStatus(board) {}
ที่รับสถานะของตารางเกม O-X ในพารามิเตอร์ board ที่อยู่ในรูปของ Array ที่ประกอบด้วยสตริง 3 ตัว แต่ละตัวประกอบด้วยอักษร 'X', 'O', '.' ความยาว 3 ตัวอักษร
ตัวอย่างการเรียกใช้เป็นดังด้านล่าง
describe('OXStatus', function(){
it('should tell X wins with the center vertical column', function(){
assert(OXStatus(['OX.',
'OX.',
'.XO'])=='X wins');
});
});
ค่าที่คืนเป็นสตริง โดยในการเขียน คุณจะใช้ค่าอะไรแทนสถานะอย่างไรก็ได้ตามสะดวก อย่างไรก็ตามฟังก์ชันควรแยกแยะกรณีที่ X ชนะ, O ชนะ, เสมอ, เกมยังไม่จบ, หรือเกมเป็นไปไม่ได้ ได้
แบบฝึกหัด 2: วัตถุและสถานะ - Tennis Game
ด้านล่างเป็นโครงของคลาส
function TennisGame() {
this.scoreA = 0;
this.scoreB = 0;
}
TennisGame.prototype.getStatus = function() {
return 'a:0-b:0';
};
TennisGame.prototype.aScore = function() {};
TennisGame.prototype.bScore = function() {};
ด้านล่างเป็นตัวอย่างของบาง test
describe('TennisGame', function(){
var tennisGame = null;
beforeEach(function(){
tennisGame = new TennisGame();
});
it('should return status when the game starts', function(){
assert.equal(tennisGame.getStatus(),'a:0-b:0');
});
it('should return status when A score one point', function(){
tennisGame.aScore();
assert.equal(tennisGame.getStatus(),'a:15-b:0');
});
});