Unit test ตอนที่ 3 งานจริง เขียนจริง แก้จริง รักจริง

งานจริง เขียนจริง แก้จริง รักจริง

งานจริง

ผมเขียนเกี่ยวกับ Unit test ไว้ 4 ตอน คุณสามารถกด Link ด้านล่างเพื่ออ่านที่เกี่ยวกับ Unit test ตอนต่างๆได้เลย

ผมได้รับมอบหมายงานตัวนึงเป็นง่ายไม่ซับซ้อนมาก (แต่ผมทำให้มันซับซ้อน) มันคืองานทำตัว FTP ไว้ Download และ Upload ไฟล์ ปัญหามันเกิดจากลูกค้าเนี่ยไม่จะไม่ยอม Implement ช่องทางการส่งข้อมูลเลย ไม่ว่าจะทำ API หรือ ทำ Queue หรืออะไรก็แล้วแต่ เขาจะทำอย่างเดียวคือ FTP ครับ คือจริงๆผมไม่ค่อยชอบการรับส่งข้อมูลประเภทนี้เท่าไหร่ เพราะมันไม่มีการแจ้ง Error ห่าเหวอะไรเลยระหว่างคนส่งคนรับให้ทราบเลย บางทีอ่านไฟล์มาแล้วไฟล์แม่งไม่ถูก Format เราก็ไม่รู้จะทำยังไงเพื่อบอกว่ามัน Error

  • ส่งเมลล์ไปบอกเหรอว่า Error ประเด็นคือแม่งอ่านเมลล์รึเปล่า
  • สร้างไฟล์บอกว่า Error ว่าอะไรแล้วส่งกลับไปให้เขาเหรอ ประเด็นคือ API แม่งยังไม่ยอมเขียนเลย แม่งจะมีมาเขียนอ่านว่า Error อะไรเหรอ

ไอกรณีแบบนี้แม่งเกิดขึ้นแล้วในงานที่ผมดูแล แล้วก็คือเวลามัน Error เราก็ไปบอกเขาไม่ได้ว่า Error พอมีปัญหาลูกค้าแม่งก็มาโบ้ยว่าทางผมผิด ผมก็ต้องมานั่งเสียเวลาหาว่าอะไรมันผิดแล้วเอา Log ไปยืนยันว่าไม่ผิด แล้วแจ้งให้เขาทราบ แต่อย่างว่าฝั่งเขาไม่เสียอะไร มีแต่ผมที่เสีย ในสถานการณ์แบบนี้เขาได้เปรียบเขาไม่ทำอะไรหรอกครับ

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

โจทย์ที่ได้รับ

โจทย์ที่ได้มาคือเขียน Program ที่

  1. มันใช้รับส่ง FTP ได้ โดยทำได้ทั้ง Upload และ Download
  2. ต้องรองรับทั้ง FTP, FTPS, SFTP
  3. ในขั้นตอนการ Download เมื่อ Download เสร็จแล้ว สามารถเลือกได้ว่าจะ Delete file ทิ้ง หรือ Move file ไปไว้ที่ Directory อื่นของเครื่อง Server
  4. สามารถเลือก File Extension อะไรที่ Download หรือ Upload เช่น จะเอาเฉพาะไฟล์ xml txt เท่านั้น

Design แบบกระโหลกกะลาปลากระป๋อง

ผม Design Control flow ของ Upload และ Download ประมาณนี้ โดยเริ่มจาก Connection List file แล้วเอามาทำ Upload หรือ Download จากนั้นทำการ Clear ข้อมูล ถ้าสำเร็จบันทึกเข้า Success List ถ้าไม่สำเร็จเอาเข้า Fail List ดูง่ายๆเนอะ บางคนอาจบอกว่า Class กระจอกแบบนี้ เขียน 2 ชั่วโมงเสร็จ ครับ 2 ชั่วโมงอาจจะเสร็จจริงๆครับ แต่ผมใช้เวลาประมาณ 1 สัปดาห์ (สกิลการ Coding ของผมกากมากๆครับ)

เงื่อนไขต่อไปที่ผมคิดขึ้นมาคือ ผมจะเรียกใช้มันทุก 10 วินาที ซึ่งมันเป็น Schedule Task ซึ่งพอจะทำเป็น Schecdule Task ตัวที่จะถูกเรียกใช้งานจะต้อง Implement Runable Interface แล้วตัว Schecdule Task จะเรียกใช้งานตัว method run() ทุก 10 วินาที

คิด Case ก่อนเขียน

ถ้าจะเขียน Unit test ต้องคิด Case เขียน Unit test ก่อนเขียน Code เสมอ แต่คุณสามารถคิดวิธีเขียน Code ไปในหัว เขียน Flow ก็ได้

เริ่ม Download Task ก่อน

โอเคมันจะมี Case อะไรบ้าง

  • ถ้า Connect แล้ว Error จะทำยังไง ตรวจสอบยังไงว่ามันทำงานถูกต้อง
  • ถ้า List file บน Server แล้วเกิด Error จะทำยังไง ตรวจสอบยังไงว่ามันทำงานถูกต้อง
  • ถ้า Save file ลง Local ไม่สำเร็จจะทำยังไง ตรวจสอบยังไงว่ามันทำงานถูกต้อง
  • ถ้า Clean up file ไม่สำเร็จจะทำยังไง ตรวจสอบยังไงว่ามันทำงานถูกต้อง
  • ถ้าห่มผ้าเองแล้วมันไม่หนาว อันนี้ไม่เกี่ยวเป็นมุก

ลางร้ายเริ่มปรากฎ (แต่ผมไม่ได้เป็นคนแต่งเรื่องที่กำลังเล่าเรื่อง แล้วก็ไม่ได้มีเด็ก 3 คนเดินเรื่อง)

มันจะเขียน Test ยังไงวะ มันไม่ Return ค่าออกมา (เนื่องจากใช้ method : run() ในการทำงาน) จะ Assert มันยังไง นี่เป็นปัญหาอย่างแรกๆที่เราเขียน Unit test บางหลักการเขียน Code ถึงกับบอกว่า Function ที่ไม่มี Return เนี่ย เป็น Function ที่ไม่ปกติ ไม่สะอาด เขามองว่าถ้าสั่งแบบนี้มันมี ผลข้างเคียง ลองอ่านเพิ่มเติมเกี่ยวกับ Functional Programming ได้ครับ สนุกดี ผมชอบหลายๆอย่าง ชอบตรงแนวคิดที่ใส่ A และ B เข้า Function เดิมมันก็ควรได้ค่าเดิมตลอด เพราะมันคือ Function ตามคณิตศาสตร์ แต่ผมไม่ชอบเรื่องเวลาต่อ Function ยาวๆ แล้วมันวงเล็บซ้อนวงเล็บ อ่านแล้วมันชวนอ้วกแปลกๆ

ตัวอย่าง Unit test ที่เขียน

ค่อยๆดูปัญหาแต่ละข้อครับ

run_NormalCase_NoIdeaToVerify

ตัวอย่างนี้จะเห็นว่าผมไม่รู้ว่าจะ Assert ค่ายังไง พีคจริงๆครับอันนี้ จริงๆผมมีวิธีเลี่ยงที่คิดไว้ในหัวละคือสร้าง List ที่เก็บ Success กับ Fail แล้วสร้าง getter setter ออกมา Assert แต่พอจะเห็นปัญหาแล้วนะครับว่า ถ้าเขียน Function ไม่มี Return มันจะตรวจสอบยาก ต้องหาวิธีทำอ้อมๆ อันนี้โชคดีที่เป็น Object ต้องสร้าง instance เลยมีตัวแปรอื่นๆได้และไม่ต้องกลัว Race condition (กรณีถ้าคุณไม่เอา Object นี้ไปใช้หลายๆ Thread ) แต่ถ้าเป็น static Class แล้วคุณสร้างตัวแปร List ที่เก็บ Success กับ Fail ตัวแปรที่สร้างจะเป็น static คราวนี้มันจะกลายเป็น Global variable แล้วหลาย Thread สามารถเรียกใช้มันพร้อมกันได้ง่ายๆ (จากความไม่รู้) และเกิด Bug แบบงงๆ แบบเกิดบ้างไม่เกิดบ้าง

run_FtpConnectionError_ThrowIOException และ run_FtpListFileError_ThrowIOException

อันนี้เนื่องจาก method : run() ของ Runable ห้าม Throw Exception ออกมา ทำให้เราไม่สาารถดัก Exception อะไรได้เลย

  • ผมจะรู้ได้ไงว่ามัน Error ตอนไหนกันแน่ระหว่าง Connection หรือ ListFile แล้วผมจะรู้ได้ไงว่ามันทำการ Connection ก่อน ListFile แล้วผมจะรู้ได้ไงว่า ถ้า ListFile แล้วได้ Error แล้วมันมีการ Close connection จริง (ผมอยากรู้เพราะว่ามันต้องพิสูจน์ว่า Connection ถูกปิดหลังจากเกิดปัญหา การ Clanup Resource เป็นเรื่องที่โปรแกรมเมอร์ต้องทำ)

  • แล้วผมจะทำยังไงให้มัน Connection Error แล้วทำยังไงให้มัน ListFile Error โดยไม่ต้องแก้ Code Unit test หรือ Config ทุกครั้งที่ Run ลองนึกภาพตอน Test แล้วต้องสร้างเครื่อง Ftp server จะ Test ปกติต้องตั้งค่าให้ถูก จะ Test ไม่ปกติต้องไปตั้งค่าให้ผิด แล้วมันจะ Test ครั้งเดียวแล้วรู้ได้ไงว่ามันถูก แก้ไปแก้มาปวดหัวตาย

จะแก้เรื่องนี้ต้องแก้ที่ Design

ปัญหาทั้งหมดถ้าจะแก้เราต้องแก้ Design ครับ เพราะ Code ที่เราเขียนตอนนี้ไม่เปิดโอกาสให้เรา Test ได้ง่ายๆเลย ถ้าเขียน Code แบบตรงไปตรงมาจะได้ Code ประมาณนี้ เอ่อ ไม่อยากเขียนเลยอะ มันยาวมาก ขอสมมติแทนส่วนที่เป็นพวก Connection , Download , Cleanup นะครับ

จาก Code ที่ผมเขียนจะเห็นว่าการทำงานทุกอย่างนั้นอยู่ใน Class : FtpDownloadTask หมดเลย ซึ่งเราไม่สามารถแทรกแทรงอะไรตอน Test ได้เลย Code จะทำงานตามที่เขียนอย่างงั้นเลย เวลาจะสร้างเหตุการณ์ Error ต่างๆนั้นต้องไปทำที่ตัว FTP Server ซึ่งมันจะไม่เป็นไปตามที่ผมเคยบอกว่าไว้ว่า : โดยไม่ต้องติดต่อกับส่วนจริงๆที่เกี่ยวข้องกับ Code ส่วนนั้น วิธีกจะแก้ปัญหานี้เราต้องทำการแก้ Design ของ Code : FtpDownloadTask เสียใหม่ แต่ก่อนที่จะทำแบบนั้น ผมขออธิบายอีกเรื่องหนึ่งก่อนจะไปเรื่องแก้เรื่อง Design

Mock(ingjay)

พอเขียน Unit test มาถึงระดับนึง มันจะมีปัญหานึงเกิดขึ้นมาเมื่อเราต้องเรียกใช้งาน Component อื่น เช่น

เราได้โจทย์ให้เขียน
Class : DebtManagement : Method : public List findCompanyToAlertDebt(Integer debt)

โดย step การทำงานคือดึงข้อมูลลูกค้าออกมาจากแหล่งข้อมูล จากนั้นทำค้นหาว่าลูกค้าเจ้าไหนบ้างที่มีหนี้มากกว่าหรือเท่ากับที่เรากำหนด ถ้าเกินกว่าก็จะ Return ค่าออกมาเป็น List บริษัทที่เกิน โดยส่วนทีทำการดึงข้อมูลนั้นถูกแยกให้ออกจาก DebtManagement และให้มาเพียง Interface ประมาณนี้

ซึ่งตัว Class จริงๆยังไม่มีคนเขียน และคุณเองก็ถูกสั่งห้ามเขียน(จริงๆไม่ได้ห้ามทำงานไม่ทัน) เพราะคุณถูกมอบหมายให้เขียนส่วนจัดการหนี้ และ ส่วน Business Logic อื่นๆ ปัญหาคือคุณจะเขียน Unit test ยังไง และมันจะ Test ได้ยังไง เพราะตัวที่เป็น Dao จริงๆไม่มี มีแต่ Interface

  • วิธีธรรมดาที่เราพอคิดได้

ผมก็ไปเขียน Class ที่มัน Implement Interface : CustomerDebtDao แล้วให้มี Return ค่าง่ายๆออกมาตามเงื่อนไข ถ้ามีหลาย Case ผมก็ไปเพิ่ม If else เข้าไปเพิ่ม เช่น เรียกรอบแรก return ออกมา 10 ตัว เรียกรอบที่สอง return ออกมา 5 ตัว ตามแต่ Case หรือ สามารถตั้งเงื่อนไขต่างๆได้

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

อธิบาย Code กัน

Code อาจมากแต่จริงส่วนสำคัญมีนิดเดียว

CustomerDebtDao customerDebtDao = mock(CustomerDebtDao.class);

ส่วนนี้เป็นการจำลอง CustomerDebtDao ขึ้นมาโดยใช้ method mock ของ mockito คือ อยากจำลองอะไรก็เอาใส่ class นั้นเข้าไปเลย จากนั้น mockito จะ return obj ทีแทน class นั้นออกมาได้เลย ตัวอย่างของผมนี่มีแค่ Interface ก็สามารถจำลอง Class ออกมาได้แล้ว โคตรเจ๋ง

when(customerDebtDao.findAll()).thenReturn(returnData);

ส่วนอันนี้เป็นการจำลอง Case ต่างๆ โดยตัวอย่างนี้เราจะทำการจำลอง method : findAll โดยผมจำลองให้มัน return ค่าออกมาตามที่กำหนด เพื่อให้เป็น Case ต่างๆเช่น Case ปกติ Case ที่มีค่าซ้ำ เพื่อดูว่าตัว findCompanyToAlertDebt ทำงานถูกต้องไหม

when(customerDebtDao.findAll()).thenThrow(IOException.class);

เมื่อกี้มีสร้าง Case ปกติแล้ว อันนี้คือการสร้าง Case Exception

ตัดจบแขวนคนดู

รู้สึกตอนนี้จะเริ่มยาวเกินไปละ ตอนแรกว่าจะเอาให้จบเลยแต่ยิ่งเขียนยิ่งยาว พอยาวมากๆคนจะไม่อยากอ่าน เลยขอตัดตรงนี้เลยละกัน ในตอนนี้ผมได้ยกตัวอย่างงานจริงที่ผมได้รับ แล้วพอเอามาเขียน Unit Test ก็ติดปัญหาว่าจะ Assert ค่ายังไง จะจำลอง Case ต่างๆยังไง ซึ่งวิธีแก้ปัญหาคือทำการแก้ Design ซึ่งมันจะอธิบายยาวมาก เลยขอแยกมาอธิบายเรื่องการ Mock ก่อนว่ามันเป็นยังไง หลักการเขียนมันประมาณไหน แล้วค่อยไปอธิบายการแก้ Design ในตอนหน้าแทน

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

เพลงประกอบการเขียนบทความนี้

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

ref :

https://th.wikipedia.org/wiki/%E0%B8%AD%E0%B8%A2%E0%B8%B2%E0%B8%81%E0%B9%83%E0%B8%AB%E0%B9%89%E0%B9%80%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B8%99%E0%B8%B5%E0%B9%89%E0%B9%84%E0%B8%A1%E0%B9%88%E0%B8%A1%E0%B8%B5%E0%B9%82%E0%B8%8A%E0%B8%84%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%A2
https://th.wikipedia.org/wiki/%E0%B8%A1%E0%B9%87%E0%B8%AD%E0%B8%81%E0%B8%81%E0%B8%B4%E0%B9%89%E0%B8%87%E0%B9%80%E0%B8%88%E0%B8%A2%E0%B9%8C
https://www.facebook.com/bnk48official.kaew/