มาฟังผมฝอยเรื่อง unit test กัน

ผมเป็นแฟนตัวยงของการเขียน automate test (ต่อไปผมจะย่อสั้นๆ เหลือแค่ test นะครับ) ถึงขั้นใช้เป็นส่วนหนึ่งในการตัดสินใจเลือกเข้าทำงานที่ใดๆ เลยทีเดียว เพราะผมเชื่อว่าการเขียน test อย่างจริงจัง มีประโยชน์มากมาย ทั้งเป็นส่วนหนึ่งที่จะทำให้เราทำงานได้ทันเวลามากขึ้น มีความมั่นใจมากขึ้นเมื่อจะ deploy ขึ้น production แก้ไขปรับปรุงโปรแกรมเราได้อย่างมั่นใจ ว่าไม่ไปทำให้สิ่งที่เคยทำมาพัง …

แต่การเขียน test นั้นไม่ง่ายนัก เพราะมีหลายเรื่องที่ต้องคำนึงถึง หากเขียน test ไม่ดี จะทำให้เสียเวลาการทำงานมากขึ้นโดยเปล่าประโยชน์ และเป็นภาระต่อไปในอนาคต ดังที่ผมได้เคยไปสร้าง bad impression และ code ที่ไม่ค่อยมีประโยชน์เท่าไหร่ไว้ และส่วนตัวผมเคยพยายามเขียนถึงเรื่องการเขียน test ไปแล้วครั้งหนึ่ง แต่รู้สึกว่าทำได้ไม่ดีเท่าไหร่

ช่วงนี้ได้มีโอกาสดูเทปบันทึก RubyConf2011 ถึงมันจะมีตั้ง 65 talk แต่แน่นอน ผมเลือกดูเรื่องเกี่ยวกับ testing เป็นเรื่องแรกๆ

แต่ก่อนที่ผมจะไปเล่าถึงเรื่อง talk ที่ผมได้ดูนั้น อยากจะทำความเค้าใจกันนิดนึง

สำหรับ unit testing ในโลก OOP (test ระดับอื่นไม่เกี่ยวในวันนี้) ปัจจุบันนี้มีสไตล์การเขียนอยู่ 2 แบบ คือ

  1. Interaction testing (mock) คือ การ test message ที่ object ส่งหากัน ด้วยการใช้ mock ไปตรวจสอบการ call method ระหว่าง object อาจจะมองได้ว่าเป็นการทำ white box testing
  2. State testing คือ การเรียก method แล้วตรวจสอบผลลัพธ์ที่ออกมา อาจมองได้ว่าเป็นการทำ black box testing

ใน blog นี้จะกล่าวถึง 2 แบบนี้อยู่เรื่อยๆ นะครับ

talk เรื่องแรกที่ผมเลือกดูคือ เรื่อง Your tests are lying to you

เรื่องนี้เค้าเล่าถึงปัญหาของการใช้ mock, stub (1) อย่างผิดวิธี ทำให้ test มัน brittle (เกิด false-positive และ true-negative ขึ้นง่าย), ต้อง setup กันวุ่นวาย และเกิด code duplication ระหว่าง test และ code เสียเวลา และต้องการการ maintain สูง

จากนั้นเค้าได้ลองเปลี่ยนไปใช้แบบที่ 2 ซึ่งแก้ปัญหาในขั้นต้น แต่เค้ากลับพบปัญหาใหม่ คือ การที่ test ไม่ครอบคลุม(เพราะ blackbox) และ test รันได้ช้า(เพราะเกี่ยวเนื่องกับสิ่งที่ไม่ได้ต้องการจะ test ณ เวลาใดๆ เยอะ)

สุดท้ายเค้าจึงแก้ปัญหาด้วยการใช้วิธีที่ 1 อย่างถูกวิธีมากขึ้น ท้ายการพูดมีการ discussion กันหนิดหน่อย เรื่องสไตล์ mock กับสไตล์สร้าง class จริง

อีก talk นึงที่ผมเลือกดูคือเรื่องนี้ Why You Don’t Get Mock Objects

เรื่องนี้เค้าได้แรงบันดาลในมาจากหนังสือชื่อว่า Growing Object-Oriented Software Guided by Tests หนังสือเล่มนี้เป็นหนังสือคอมฯ ภาษาอังกฤษเล่มเดียว(ถ้าไม่นับหนังสือเรียนป.ตรี)ที่ผมมีเป็นเจ้าของ เพราะที่ผ่านมาอ่านแต่ของเถื่อน 

หนังสือเล่มนี้สนับสนุน test สไตล์ที่ 1 อย่างจริงจัง สมัยผมอ่านผมก็ยังงงๆ เพราะมันทำให้การเขียนโปรแกรมของผมต่างไปจากที่คนอื่นเขียนทั่วไปมากเพราะ หนังสือเล่มนี้ทำตาม SOLID และ Tell, Don’t Ask อย่างเคร่งครัด

ตัวอย่าง เช่น การมี getter เป็นบาป การที่เรา call method เพื่อเอา value ข้างในออกไปทำอย่างอื่นเป็นเรื่องที่ไม่ถูกต้อง เพราะถือว่าเป็นการนำ property ภายในของ object ออกมา ผิดหลัก information hiding

อีกตัวอย่าง คือ การส่ง message ระหว่าง object (call method) ไม่ควร call ที่ concrete class ตรงๆ เพราะจะเป็นการสร้าง tightly coupling ระหว่าง class นั้นๆ

ผลดีอย่างหนึ่งที่สังเกตได้อย่างชัดเจนหลังจากลองเขียน code สไตล์หนังสือนี้ คือ ทำให้เรา call method โดย pass parameter ที่เกี่ยวข้องกับบทบาทของ class มากขึ้น จำนวน parameter น้อยลง เหตุการณ์แบบว่าเขียน class อยู่ที่นึง แล้วต้อง pass value จากที่อื่นผ่านหลายชั้นกว่าจะมาถึง class ที่ต้องการมีน้อยลง (การ pass value ผ่าน object ที่ไม่ได้ใช้มันจริงๆ เป็น smell ของ design ที่ไม่ดีอย่างหนึ่ง เพราะมันแปลว่า class ทางผ่านนั้น มี dependency กับ value ที่วิ่งผ่าน)

อย่างไรก็ตามครั้งที่ผมอ่านหนังสือเล่มนี้ ผมยังไม่ค่อยเข้าใจ 100% นำไปใช้อย่างงูๆ ปลาๆ บ้าง ส่งผลดีบ้างร้ายบ้าง

หลังจากผมดู talk นี้จบ และผมได้ไป check out code ที่ผู้พูดเค้าเขียน style นี้เป็นตัวอย่างไว้ให้ดู มาไล่ดู ผมก็เข้าใจอะไรบางอย่างมากขึ้น

มุมมองของผม ณ ปัจจุบัน (วันนี้)

  • จาก code ที่ผมได้ไปเอามาดู ผมพบว่าสุดท้าย เค้าก็มีการทำ State testing อยู่ดี แต่เป็นที่ชั้นสุดท้ายของ object graph ของเค้าเลย
  • การเขียน code แบบ Mocking style(แบบที่ 1) นีี้ทำให้ไล่ code ยากมากเพราะแต่ละ object จะ call ที่ abstraction ตอนเขียน Java มี IDE ช่วยว่าไล่ยากแล้ว มาเจอบน Ruby แทบแย่
  • Mocking style นี้ ถ้าเขียนดีๆ(ใช้ความชำนาญสูงมาก) จะทำให้เราเขียน code โดยเริ่มจาก system เล็กๆ และค่อยๆ เพิ่ม feature มากขึ้นโดยแทบไม่ต้องแก้ไข concrete class เก่าเลยได้อย่างน่าอัศจรรย์ใจมาก ซึ่งทำให้เสี่ยงที่จะเกิด bug น้อยลง
  • State testing นี่ทำให้ test ช้าจริงๆ ยิ่งถ้าทำงานกับ dependency อื่นๆ เช่น database ด้วยจะยิ่งช้ามาก ตอนนี้เจอกับตัว automate test server ของที่บริษัท รันแต่ละครั้งใช้เวลากว่า 20 นาที ซึ่งผมมองว่ามันช้าจนเริ่มมีผลเสีย
  • ปัจจุบันนี้เรามักจะใช้วิธีการเทสแบบ State testing โดยการเพิ่ม feature จะมีการแก้ code เก่าบางส่วน เพิ่ม code ใหม่บางส่วน ใครมี test คลุมไว้และ update เพิ่มอย่างสม่ำเสมอก็จะดีหน่อย แต่ก็จะพบว่า test มัน brittle ถ้ารับได้ก็โอเค แต่ส่วนคนไม่มี test ก็ใช้การคาดเดาและ manual test เอา ซึ่งก็จะเสี่ยงหน่อย ตามความซับซ้อน

สาเหตุแม้ปัจจุบันคนจะนิยมทำ state testing แต่ก็ไม่มีปัญหามากมายนัก ถึงขั้นที่ทำให้ mock testing (แบบถูกวิธี) เป็นที่นิยมขึ้นมาได้ ผมเข้าใจว่าเพราะเหตุผลต่อไปนี้

  • เราใช้ประสบการณ์ในการตัดแบ่ง abstraction ตั้งแต่ก่อนจะเริ่มทำจริง ผลออกมาดีแค่ไหน จึงขึ้นอยู่กับประสบการณ์และความสามารถ
  • เรามีการ call ไปที่ abstraction อยู่เหมือนกัน แต่ abstraction ด้วย software ownership เช่น การเรียก web service, การเรียกใช้ library ของคนอื่น ทำให้ความซับซ้อนและความเหนื่อยที่จะต้อง maintain ไม่สูงมากนัก
  • งานส่วนใหญ่เป็น CRUD มันไม่ซับซ้อนมากนัก
  • เรามี framework ต่างๆ เช่น MVC ที่ทำให้ OOP ที่ไม่ค่อยถูกต้อง(อ้างอิงจาก SOLID และ TDA) มันใช้ง่าย maintain ง่าย หลายๆ อย่างก็ไม่ต้อง test เพราะมีคน test ให้แล้ว

วิธีคิดในการเขียน test ต่อๆ ไป

ตอนนี้ผมก็รู้ข้อดีข้อเสีย และวิธีการเขียน test ทั้งสองแบบที่(น่าจะ)ถูกต้องมากขึ้น ผมก็คง mix ใช้ทั้งสองอย่าง โดยใช้การสังเกตจากผลลัพธ์ code ที่ออกมาดูว่ามันใช้ถูกหรือผิดอย่างไร คร่าวๆ ดังนี้

  • พยายามใช้ mocking style เท่าที่เป็นไปได้
  • คอยจับตาการเกิด code duplication ระหว่าง test กับ code
  • คอยจับตาการทำ test setup เยอะเกินไป
  • คอยจับตา test brittle
  • ทำ state testing กับ object อื่นๆ โดย พยายาม keep ให้ object นั้นๆ ทำงานไม่ซับซ้อนมาก เพื่อป้องกันการ test ไม่ครอบคลุม หากมันเริ่มทำอะไรหลายอย่าง ก็แตกมันออกซะ
  • แต่เชื่อว่าสุดท้ายแล้วช่วงนี้ก็คงเขียน state testing ซะส่วนมาก เพราะ Rails มันเป็น data-driven framework และเพื่อนร่วมงานก็เขียนสไตล์นี้กัน

ถ้ารู้อะไรมากขึ้น เปลี่ยนไปยังไง เดี๋ยวจะมา update อีกทีนะครับ

เห้อ ยาวมากนี่แค่ unit testing นะเนี้ย

Advertisements

8 thoughts on “มาฟังผมฝอยเรื่อง unit test กัน

  1. Maythee Anegboonlap

    รู้สึกว่าทั้งสองอย่างมันต้องใช้ทั้งคู่เพียงแต่ว่าใช้ตอนไหนแฮะ หลังจากที่ทดลองมาซักพักพบว่า State testing จะใช้กับ Component หรือ Class ในระบบที่เราสามารถควบคุมพฤติกรรมได้ และไม่จำเป็นต้องสร้าง Mock เพื่อให้มี Concrete class ที่อาจมีพฤติกรรมที่ต่างจาก Class จริงส่วนแบบแรก Interaction testing จะใช้กับ Class หรือ Component ที่เป็น Third party และ Class ที่ต้องใช้คู่กันเท่านั้น ง่ายๆ ก็คือหา Third party ขึ้นมาก่อนเลย แม้จะเป็น Component ที่เขียนเองแต่ว่าไม่ใช่ส่วนสำคัญของระบบก็ถือเป็น Third party แล้ว Mock มันขึ้นมา จากนั้นก็ Test ตามผลลัพธ์ที่คิดว่า Third party จะให้ผลลัพธ์ออกมาทำมาได้สองสามเดือนก็รู้สึกมีความสุขดีนะ เขียน Mock ไม่รู้สึกทรมานมาก เพราะมันไม่เยอะเกินไปขนาดเดียวกันก็เขียนได้ครอบคลุมเกือบทุกส่วนและใช้เวลาไม่เยอะ รัน unit test ได้เร็วๆ ไม่เกินนาที ตอนนี้เป้าหมายต่อไปคือทำ automate test ส่วนที่เป็น functional test ที่กลายเป็นปัญหามากกว่าหละ

    Reply
  2. Maythee Anegboonlap

    มีข้อสงสัยเพิ่มเติมอีกเล็กน้อย Automate server รันที 20 นาทีนี่แค่ Integration test จริงหรอ รู้สึกว่ามันนานเกินจะเป็นแค่ Unit test หละ ฮะๆ เพราะถ้าเป็น Functional test ที่ต้องมี Fixture data ใน DB จริงๆ ก็ยอมรับได้นะ เพราะมันเป็นการทดสอบทั้ง function

    Reply
  3. Nuttanart Pornprasitsakul

    – ในหนังสือ(และ presentaion)เค้าบอกว่าอย่า mock boundary เพราะไม่มีประโยชน์- วิธีที่พี่ใช้ เค้าจะเรียกว่า stub อะครับ มีประเด็นเรื่องชื่อกันอยู่พอสมควร เช่น link นี้ http://martinfowler.com/articles/mocksArentStubs.html- ที่บริษัทคนในบริษัทเค้าเรียกว่า unit test แต่จริงๆ มันไม่ใช่ เพราะมัน query db จริงบ้าง, class บาง class ก็ใหญ่เกินจะเป็น class เดียวบ้าว

    Reply
  4. Maythee Anegboonlap

    ตอบข้อสองก่อน มันไม่ใช่ Stub อะเพราะมันมี generate fail case ด้วย และก็ยังคิดว่าควรจะทำที่ boundary มากกว่าทำทุก component เพราะ boundary นี่แหละที่ให้ fixture data ง่ายสุด และทำให้ไม่เหนื่อยเกินไปส่วนข้อแรก ไม่ชอบให้ Mock class กลางๆ component มากเพราะมันจะทำให้เหนื่อยมากขึ้นเยอะ และไม่สามารถบอกได้ว่าต้องทำที่ class ไหนบ้าง อะไรควรเอามาทำเป็น Mock (ยกเว้นว่ามีข้อกำหนดพวกนี้นะ ถึงจะเห็นด้วย)ข้อสุดท้าย ตอนนี้ให้แยกออกจากกันไปเลย ถ้าต้อง query database จริงให้ยอมทำได้น้อยลง (จริงๆ พยายามทำ automate run ตัวนี้อยู่ด้วยเฉพาะเลย ทั้ง automate load fixture, start stop service ต่างๆ) แต่ unit test/integration test dev ต้องรันบ่อยที่สุด แทบจะตลอดเวลา และมีข้อกำหนดอย่างหนึ่งคือต้องไม่มี wait และต้อรันเสร็จไม่เกินวินาที

    Reply
  5. Nuttanart Pornprasitsakul

    – generate result หรือ error ตามที่เค้าพูดกันก็คือ stub อะครับ ถ้าสุดท้ายพี่ตรวจสอบความถูกต้อง (สำหรับ case ที่ไม่ได้ fail) ด้วย == ค่าใดๆ นั่นคือพี่ test state ครับ- พวกตรวจสอบถูกผิดด้วยการ expect receive อันนี้ ถึงเป็น mock ตามนิยามพวกเค้า- ย่อหน้าสุดท้ายดีแล้วครับ ส่วนบริษัทผม ผมไปทำอะไรไม่ได้มาก ต้องหลิ่วตาตามไปก่อน แต่ถ้าเวลาเราทำก็พยายามทำให้ถูก

    Reply
  6. Nuttanart Pornprasitsakul

    เมื่อกี้โพสผิด ต้องแค่นี้ทำการ validate test ว่าผ่านไม่ผ่านด้วย expression พวกนี้อะครับexpect(x).toHaveBeenCalled() passes if x is a spy and was calledexpect(x).toHaveBeenCalledWith(arguments) passes if x is a spy and was called with the specified argumentsexpect(x).not.toHaveBeenCalled() passes if x is a spy and was not calledexpect(x).not.toHaveBeenCalledWith(ar

    Reply
  7. Maythee Anegboonlap

    โอ รู้สึกคำจำกัดความมันต่างกันเยอะเหมือนกันแฮะ

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s