in Technology

ต้องคิดอย่างไร รู้อะไร และทำอย่างไร เพื่อให้เกิด Automated Test

หลังจากได้เขียนอธิบายเรื่อง “จะทำระบบให้รองรับ Automated Test ได้อย่างไร (Testable)” ไปคราวก่อน ได้รับผลตอบรับด้วยดี คราวนี้เลยมาเขียนเพิ่มเติมเพื่อเป็นตัวอย่างแก่ผู้ที่ต้องการนำไปพัฒนาระบบจริง โดยในบล็อกนี้ผู้อ่านจะได้เห็นภาพของสิ่งเหล่านี้ คือ

  • กระบวนการคิดเพื่อเตรียมทำ Automated Test
  • Unit Test
  • Integration Test
  • DI (Dependency Injection)
  • Stub (Test Double)
  • Code Coverage

ระบบที่จะทำในบล็อกนี้

ผมต้องการทำระบบคิดเงินอัตราแลกเปลี่ยน โดยการดึงข้อมูลอัตราแลกเปลี่ยนกลางจาก API ของธนาคารแห่งประเทศไทย และบวกค่าธรรมเนียมการให้บริการไปด้วย (ทำตัวเป็นคนกลางรับแลกเปลี่ยนเงินตรา) โดยรายละเอียดของเงื่อนไขมีดังนี้

  • สามารถแลกเงินได้ตั้งแต่ 1$ ขึ้นไป
  • ต้องแลกเป็นเงินจำนวนเต็ม 1$ เท่านั้น ไม่สามารถแลกเงินเป็นเศษได้
  • บวกค่าธรรมเนียมการแลกเงิน 2.50% จากราคาปกติ เมื่อลูกค้าแลกเงินในช่วง 1$ – 100$
  • บวกค่าธรรมเนียมการแลกเงิน 2.15% จากราคาปกติ เมื่อลูกค้าแลกเงินในช่วง 101$-500$
  • บวกค่าธรรมเนียมการแลกเงิน 2.00% จากราคาปกติ เมื่อลูกค้าแลกเงินตั้งแต่ 501$ ขึ้นไป
  • แสดงค่าอัตราแลกเปลี่ยนเป็นทศนิยม 2 ตำแหน่ง ถ้ามีเศษให้ปัดขึ้นเสมอ โดยพิจารณาที่ตำแหน่งที่ 2 (ตัวอย่าง 1 USD = 35.00THB + 2.5% = 35.875THB = 35.88THB)
  • ให้ดึงค่าอัตราแลกเปลี่ยนจาก BOT API ที่ https://iapi.bot.or.th/Developer?lang=th (ใช้ API: อัตราแลกเปลี่ยนเฉลี่ย – รายวัน)

ตัวอย่างของโจทย์นี้คือ … ผมเป็นบริษัทรับแลกเงิน จะคำนวณเงินบาท (THB) ที่ลูกค้าต้องจ่าย เพื่อแลกกับจำนวนเงินดอลลาร์ (USD) ที่ต้องการ เช่น ลูกค้าต้องการเงินจำนวน 1$ เมื่อดึงข้อมูลจากธนาคารแห่งประเทศไทยได้ 35 บาท ผมจึงบวกค่าธรรมเนียมอีก 2.5% และปัดเศษขึ้นกลมๆ แปลว่าลูกค้าต้องจ่ายให้ผม 35.88 บาท

ดังนั้นลูกค้าจะแลกเงินเท่าไร ก็จะบวกค่าธรรมเนียม และแสดงผลว่าลูกค้าต้องจ่ายเงินบาทเท่าไร ตามจำนวนเงินดอลลาร์ที่เขาต้องการ

จะทำ Automated Test ต้องเริ่มต้นที่ออกแบบ

จากโจทย์ ผมกำหนด Input, Output ง่ายๆ ให้ระบบผมก่อน ในที่นี่ Ouput คือ “เงินบาทที่ลูกค้าต้องจ่าย” ส่วน Input ผมใส่ “จำนวนเงินดอลลาร์ที่ลูกค้าต้องการ” และผมขอเพิ่ม “วันที่อ้างอิงของอัตราแลกเปลี่ยน” ที่ผมต้องการด้วย เพื่อกำหนดค่าของวันให้คงที่ ใช้ในกรณีผมต้องการทดสอบ (และเผื่ออนาคตถ้าผมอยากเปลี่ยน Business Condition ทางวันที่)

จาก Process กล่องดำๆ มืดมัว (Black Box) ผมจะแจกแจงเพื่อให้เข้าใจกระบวนการทำงานของมันเสียก่อน (White Box) ซึ่งผมถนัดคิดออกมาเป็นข้อๆก่อน จึงขอร่างไว้ดังนี้

  1. รับค่า: จำนวนเงินดอลลาร์, วันที่ของอัตราแลกเปลี่ยนเงิน
  2. ตรวจสอบจำนวนเงินมีค่าเป็นตัวเลขจำนวนเต็มบวกหรือไม่
  3. เรียกข้อมูลอัตรแลกเปลี่ยนเงินจาก API ของธนาคารแห่งประเทศไทย โดยอ้างอิงจากวันที่ต้องการ
  4. นำจำนวนเงินจำนวนเงินดอลลาร์คูณกับอัตราแลกเปลี่ยน Selling (ใช้ Selling เพราะหมายถึงอัตราที่ธนาคารขายให้เรา)
  5. นำจำนวนเงินจำนวนเงินดอลลาร์ไปหา % ของธรรมเนียมที่จะไปคิด
  6. นำจำนวนเงินบาทที่แปลงจากจำนวนเงินดอลลาร์ มาบวกกับค่าธรรมเนียมที่คิดเป็นเงินบาท
  7. นำจำนวนเงินบาทหลังรวมค่าธรรมเนียมแล้วปัดเศษให้เหลือสองหลัก โดยปัดเศษที่ตำแหน่งที่ 2
  8. คืนค่า: จำนวนเงินบาทหลังรวมค่าธรรมเนียมและปัดเศษเหลือสองตำแหน่ง

ซึ่งถ้าลองพิจารณากระบวนการดีๆแล้ว เราจะเห็นได้ว่า ข้อไหนมีการคำนวณ มีเงื่อนไขบ้างก็ประเมินไว้ก่อนเลยว่าอาจต้องทำ Unit Test และข้อไหนมีการเชื่อมต่อกับระบบภายนอกก็ต้องทำ Integration Test

คราวนี้ผมลองแปลงให้เป็น Flowchart ดู จะได้ง่ายต่อการอ่าน

มาทำ Test Cases และ Test Data กัน

ตอนนี้เราเห็นกระบวนการทำงานทั้งหมดของระบบเราแล้ว ได้เห็นเส้นทาง ทางแยกที่จะเกิดขึ้น ดังนั้น ก็พอจะสรุป Test Cases ได้แล้ว

แต่เดี๋ยวก่อน! ถ้าจะเขียน Test ให้มันทดสอบได้ แปลว่าผมคาดหวังสิ่งใดไว้ ผมจะต้องได้ค่านั้นออกมาเสมอใช่ไหม สิ่งต่างๆใน Test ของผมควรจะต้องคงที่ด้วย ดังนั้นผมจึงกำหนดค่าคงที่ให้กับวันที่เสียก่อน เพราะมันเป็นค่าเดียวที่จะทำให้อัตราแลกเปลี่ยนผิดเพี้ยนไปจากความคาดหวัง (Expect Data) ที่ผมจะเขียนไว้ใน Test

ดังนั้น ในที่นี้ผมจะกำหนดให้ใช้อัตราแลกเปลี่ยนของวันที่ 1 กุมภาพันธ์ 2561 เป็นตัวทดสอบ โดยมีอัตราแลกเปลี่ยนที่ 1$ เท่ากับ 31.5408000 THB

เราก็จะได้ Test Cases ดังนี้

Test Cases

  1. สามารถแลกเงินที่มากกว่าหรือเท่ากับ 1$ แต่ไม่เกิน 100$ ที่อ้างอิงกับอัตราแลกเปลี่ยนของวันที่ 1 กพ 2018 และบวกค่าธรรมเนียม 2.50% ได้ถูกต้อง
  2. สามารถแลกเงินที่มากกว่าหรือเท่ากับ 101$ แต่ไม่เกิน 500$ ที่อ้างอิงกับอัตราแลกเปลี่ยนของวันที่ 1 กพ 2018 และบวกค่าธรรมเนียม 2.15% ได้ถูกต้อง
  3. สามารถแลกเงินที่มากกว่าหรือเท่ากับ 501$ ขึ้นไป ที่อ้างอิงกับอัตราแลกเปลี่ยนของวันที่ 1 กพ 2018 และบวกค่าธรรมเนียม 2.00% ได้ถูกต้อง
  4. ไม่สามารถแลกเงินที่น้อยกว่า 1$ ได้

จาก 4 Test Cases ข้างต้น ผมใช้หลักการ BVA (Boundary value analysis) เป็นตัวสร้าง Test Data ให้ผม หมายความว่า ค่าใดที่ดูเป็นค่าที่จะเปลี่ยนแปลง Business Condition จะต้องเอาค่าก่อนหน้าและหลังมาใช้ด้วย เช่น 500$ ผมก็จะหยิบ 499$ และ 501$ มาทดสอบด้วย (เชื่อเถอะ ทดสอบแบบนี้ดีแล้ว เพราะโปรแกรมเมอร์ชอบมีปัญหากับเครื่องหมาย <, <=, >, >= ในการตรวจสอบค่า)

Photo: http://toolsqa.com/software-testing/boundary-value-analysis/

และเราก็จะได้ Test Data ดังนี้

Test Data

Test CasesDateChange USDSelling THBSelling THB with FeeAfter Celling
#11 Feb 2018131.540800032.3332.33
#11 Feb 20181003154.080003232.9320003232.94
#21 Feb 20181013185.6208003254.1116473254.12
#21 Feb 201849915738.85920016077.24467316077.25
#21 Feb 201850015770.40000016109.46360016109.47
#31 Feb 201850115801.94080016117.97961616117.98
#31 Feb 2018100031540.80000032171.61600032171.62
#41 Feb 20180000
#41 Feb 20180.1000

ปล. สังเกตว่า ในตารางผมมี Test Cases ประกบด้วยนะเออ เอาไว้เป็นเช็คลิสเล็กๆว่า ทำครบทุกเคสหรือไม่

ในส่วนของโค้ดนั้น

ในการเขียนโปรแกรมของโจทย์นี้ ผมใช้หลักการที่เรียกว่า TDD (Test Driven Development) ในการทำ เนื่องจากผมรู้ Test Cases รู้ Data Test ผมจึงเริ่มเขียนตั้งแต่ Test ได้เลย ..

Photo: http://haselt.com/coding-dojo-with-tdd/

หลักการของ TDD ตามรูป

  1. เริ่มจากเขียนโค้ด Unit Test ก่อนเนอะ โดยเราระบุค่า Input และค่าที่คาดหวังว่าระบบเราจะต้องส่งกลับออกมา (Output, Expect Data)
  2. พอรัน Unit Test มันก็จะไม่ผ่าน (จะผ่านได้ไงล่ะ! มันยังไม่มี Production Code ที่ใช้ทำงาน) <== กระบวนการสีแดง
  3. เขียน Production Code ที่ง่ายที่สุด เพื่อให้ Unit Test แรกมันผ่าน เช่น ใน Unit Test คาดหวังไว้ว่าต้องได้ค่าคืนมาเป็น  32.33 ผมก็จะสร้างโค้ดโง่ๆ คือ ส่งเลข 32.33 นี้ออกมาเลย <== กระบวนการสีเขียว
  4. ในการโค้ดแรกๆอาจยังเรียบๆ ไม่มีอะไร Refactoring ก็ข้ามไปก่อน <== กระบวนการ Refactor
  5. แล้วก็วนกลับไปเขียน Unit Test ใหม่ ในข้อที่สอง ตามตาราง Test Data

ซึ่งโค้ดในส่วนของ Unit Test ทั้งหมดของผม หลังจากทำครบทุกเคสและมีการ Refactor แล้ว จะหน้าตาประมาณนี้

คราวนี้ระบบของผมมีอยู่ Method หนึ่งทำการเชื่อมต่อกับ API ธนาคารของประเทศไทยด้วย ดังนั้น จะทำอย่างไรล่ะเพื่อให้เป็น Unit Test ได้ เพราะขัดหลักการของ FIRST อยู่สองข้อคือ

  • Fast: ต้องทำงานได้เร็ว <== การมี Connection ออกไปข้างนอกจะช้า
  • Repeatable: ทำซ้ำได้ ไม่ขึ้นกับระบบอื่นๆ <== มันไม่สามารถทำซ้ำได้ ถ้าระบบภายนอกเชื่อมต่อไม่ได้

และวิธีการที่เราจะทำต่อไปคือ ต้องจำลองค่า JSON ที่ส่งออกมาจาก API ธนาคารแห่งประเทศไทย ให้ได้ตามนี้

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

คราวนี้ โค้ดที่ต่อ API ธนาคารแห่งประเทศไทย มีการเรียกใช้ Library ชื่อ HttpClient ซึ่งเป็นการเรียกออกไปที่ Internet จริงๆ ดังนั้น ก็ยังผิดหลักการทำ Unit Test อยู่ดี แม้ว่าผมจะจำลองการคืนค่า JSON ได้แล้วก็ตาม

วิธีที่ผมเลือกทำคือ ผมใช้หลักการ Dependency Injection ซึ่งผมไม่ได้ให้ “Method ที่เรียก API ธนาคารแห่งประเทศไทย” เป็นตัวเรียกใช้ HttpClient แต่ผมเปลี่ยนเป็น ใครก็ตามที่เรียกใช้ “Method ที่เรียก API ธนาคารแห่งประเทศไทย” ต้องส่ง HttpClient ให้มันด้วย โดยลองดูตัวอย่างต่อไปนี้ครับ

นี่คือโค้ด BotService ที่ผมเขียนไว้เรียกใช้ API ของธนาคารแห่งประเทศไทย ที่ผมมีการส่ง HttpClient เข้ามาที่ Constructor

และนี่คือโค้ด Integration Test ของผมที่ไปเรียกใช้ BotService มันอีกที ดังนั้น ผมจะต้องส่ง HttpClient ให้มันด้วย

ซึ่งจากโค้ดดังกล่าว ใน BotService ผมไม่ใส่ HttpClient ให้กับ BotService เลย เพราะผมต้องการให้ผู้ที่เรียกใช้มันเป็นตัวบอกเองว่า จะส่ง “HttpClient จริงๆ” หรือ ส่ง “HttpClient ที่ Stub” ให้มัน โดยในตัวอย่างข้างต้น ผมเขียน Integration Test  ดังนั้น ผมจึงส่ง “HttpClient จริงๆ” ให้มันไปเลย

คราวนี้ ถ้ากลับมาดูของ Unit Test ผมต้องการส่ง”HttpClient ที่ Stub” ให้มันแทน ซึ่งในที่นี้ผมใช้ Library ชื่อ Moq (ถ้าของ Java เช่น Stubby4j) ในการทำ Stub เพื่อบอกว่า

ถ้ามีการเรียก URL  ชื่อว่า “https://iapi.bot.or.th/Stat/Stat-ExchangeRate/DAILY_AVG_EXG_RATE_V1/?start_period=2018-02-01&end_period=2018-02-01&currency=USD” จะต้องส่งค่า JSON ที่ผมต้องการออกมาเสมอ ดังนั้นจะได้โค้ดของ Unit Test ชื่อ ExchangeRateServiceTest() ดังนี้

คราวนี้ เมื่อลองรันทดสอบทั้ง Unit Test และ Integration Test ทั้งหมด จะเกิด Unit Test 9 ตัว และ Integration Test 1 ตัว

และถ้าหากเรามาเช็คเรื่อง Code Coverage ว่า โค้ดที่เราเขียนมาทั้งหมดนี้ มีการ Test ครอบคลุมครบทุกส่วนของ Business Condition หรือไม่ ผมที่ได้คือ ..

สรุป

ด้วยกระบวนการทั้งหมดที่ผมเขียนไว้ ตั้งแต่การออกแบบ จนมาทำเป็น Unit Test และ Integration Test ซึ่งถ้าทำได้ มันคือการทำให้เกิด Automated Test ดังนั้นไม่ว่าโค้ดเราจะแก้ไขอะไรไป เราก็แค่กลับมารัน Unit Test และ Integration Test ของเรา ให้มันตรวขสอบให้เราทุกเคสแบบอัตโนมัติว่ายังทำงานได้ถูกต้องหรือไม่ จากนั้นเราก็มาตรวจสอบอีกชั้นว่า นอกจากถูกต้องแล้ว ชุด Test ของเรา ยังครอบคลุมทุกบรรทัดที่เราเขียนไว้หรือไม่

เมื่อได้ Automated Test แล้ว การจะไปทำกระบวนการ Continous Integration, Continous Deployment ก็ไม่ยากแล้วครับ เพราะเรามั่นใจได้ว่าโค้ดของเรามีการทดสอบและครอบคลุมเสมอ พร้อม Deploy ไปสู่ระบบต่างๆ ได้ทันทีเมื่อเขียนระบบเสร็จ เกิดการ feedback จากผู้ใช้งานได้ไว และแก้ไขงานได้ไว มันก็จะเป็นส่วนหนึ่งที่ไปเสริมความให้เกิด Agile ในองค์กรต่อไป

โค้ดตัวอย่างทั้งหมด ดูได้ที่ https://github.com/ifew/dojo-BotExchangeRate


เพิ่มเติมทิ้งท้าย

  • ผมสังเกตว่า API Exchange Rate ของธนาคารแห่งประเทศไทย ถ้าวันไหนไม่มีอัตราแลกเปลี่ยน เช่น วันหยุด, วันเสาร์-อาทิตย์ ช่วงเวลา 00:00-17:59 จะใช้ค่าเงินล่าสุดของวันก่อนหน้านั้น แต่ถ้าวันปัจจุบันมีอัตราแลกเปลี่ยนและเวลามากกว่าหรือเท่ากับ 18:00 จะใช้ค่าเงินของวันนั้น
  • ในตัวอย่าง ผมไม่ได้ตรวจสอบว่า ถ้า API Exchange Rate ของธนาคารแห่งประเทศไทย ไม่สามารถเรียกใช้งานได้ จะต้องทำอย่างไรต่อไป
  • ทำไมต้องปัดเศษทศนิยมขึ้นที่ตำแหน่งที่สองด้วย? .. เพราะผมหน้าเลือดครับ ฮี่ๆ