ผลต่างระหว่างรุ่นของ "Afgu/unit testing 1"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
(หน้าที่ถูกสร้างด้วย 'เราใช้หัดเขียน unit test บน java script ซึ่งเป็นภาษาที่ทุกคนน...')
 
 
(ไม่แสดง 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

จาก KataTennis ที่ Coding Dojo

ด้านล่างเป็นโครงของคลาส

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');
  });
});