Applied TDD https://www.monkeyuser.com/2018/applied-tdd/

เมื่อ 2 อาทิตย์ก่อนมีโอกาสได้ไปแบ่งปันแนวปฏิบัติเกี่ยวกับ Test-driven development (TDD) ให้กับทีมพัฒนาลูกค้าฟัง เลยเอามาจดบันทึกไว้ซะหน่อยว่ามีแนวคิดในการวาง session ไว้อย่างไร เผื่อนำไปใช้กับการแบ่งปันครั้งถัด ๆ ไป

เริ่มจากรู้จักคนเรียนกันก่อน

ตอนที่เราเริ่มวางเนื้อหา เราเข้าไปเก็บข้อมูลของทีมมาก่อนก็พบว่า

ทีมไม่เคยมีประสบการณ์ในการเขียนชุดการทดสอบเลย

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

  • Project สำหรับทำ workshop ที่ขึ้นโครง testing ไว้ให้แล้ว
  • โจทย์ปัญหาที่เข้ากับ product ที่เขากำลังทำอยู่
  • ตัดสิ่งที่ไม่ใช่แก่นของ TDD ออกไป เช่น library หรือ test double

ซึ่งสรุปว่าคิดโจทย์ปัญหาได้ประมาณนี้

เขียน Vue component แปลงจำนวนเงินบาทไทยให้เป็นค่าอ่านโดยรับ props มาเป็นจำนวนเงิน (amount) ไม่ต้องคิดเศษสตางค์

จากนั้นเราก็ขึ้นโครง code ให้ประมาณนี้ โดยใช้ Vitest

การวางเนื้อหา

เราก็ย้อนกลับไปตอนสมัยหนุ่ม ๆ ว่าตอนที่เราเรียน TDD ครั้งแรกเรารู้สึกยังไง ณ ตอนนั้นมันก็มีหลากหลายความรู้สึกเข้ามา เช่น

  • ยาก มีกรณีที่ต้องครอบคลุมเยอะ
  • ตอนแรก ๆ ไม่รู้จะเริ่มอย่างไร
  • Code ที่เขียนมาทั้งหมดมันทำงานเหมือนที่คิดไว้ไหมนะ
  • Code ที่เขียนออกมาเข้าใจยาก

จากการแบ่งปันประสบการณ์ในเรื่องอื่น ๆ มาหลายครั้ง สิ่งที่ได้เรียนรู้คือ

หนึ่งในวิธีการเรียนรู้ที่ดีที่สุดคือการเชื่อมสิ่งใหม่เข้ากับสิ่งที่เคยรู้มาก่อน

เริ่ม workshop

แล้วด้วยความที่เราก็อยากให้เขารู้สึกแบบนี้ขึ้นมาด้วยสิ่งที่เขาทำเอง ก็เลยตัดสินใจว่า

เราจะให้คนเรียนทำโจทย์นี้โดยเขียน implementation code อย่างเดียวสัก 20 นาที จากนั้นให้เวลาตกตะกอนความคิดแล้วเล่าให้เพื่อนฟังว่ารู้สึกอย่างไร

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

  • ยาก มีกรณีที่ต้องครอบคลุมเยอะ -> จะแบ่งกรณีออกมาทั้งหมดอย่างไร
  • ตอนแรก ๆ ไม่รู้จะเริ่มอย่างไร -> จะเริ่มอย่างไรให้ง่าย
  • ที่เขียนมาทั้งหมดมันถูกไหมนะ -> จะทดสอบอย่างไร
  • Code ที่เขียนออกมาเข้าใจยาก -> จะปรับปรุง code อย่างไรให้มันทำงานได้เหมือนเดิม

เข้าเรื่อง TDD

แล้วก็เข้าสู่เรื่องของ TDD และกฏ 3 ข้อคือ

  1. ต้องเขียน unit test ที่ fail ขึ้นมาก่อนที่จะเขียน production code เสมอ
  2. จะไม่เขียน unit test เพิ่มที่พอ run แล้ว มันผ่าน
  3. จะไม่เขียน production code เผื่อเกินไปกว่าให้ unit test ที่ fail อยู่นั้นผ่าน

เนื่องจาก workshop มีเวลาแค่ 2 ชั่วโมงคงยากที่จะคล่อง TDD สิ่งที่เราทำคือเราตั้งความคาดหวังของเราในตัวเขาขึ้นมาว่า

วันนี้เราไม่ได้คาดหวังให้้ทุกคน “ใช้แนวปฏิบัติ TDD คล่อง” แต่คาดหวังว่าทุกคน “เห็นภาพว่า TDD มันเข้ามาเกี่ยวข้องกับความรู้สึกเมื่อกี้ได้อย่างไร”

ซึ่งตรงส่วนนี้มันมาจากแนวคิดที่ว่า

เราต้องแยก “ความรู้” ออกจาก “ทักษะ” ออกจากกัน

หนึ่งในสิ่งที่มันต่างกันก็คือ

  • “ความรู้” ถ้าแบ่งย่อย ๆ ออกเป็นส่วนเล็กพอจะมี scale อยู่ 2 อย่างคือ “รู้” กับ “ไม่รู้”
  • “ทักษะ” ถ้าแบ่งย่อย ๆ ออกเป็นส่วนเล็กพอจะมี scale มากกว่านั้น เช่น “คล่องมาก” “คล่องน้อย” “ไม่มี”

หลายคนมักจะคิดไปเองว่าเมื่อตนเองไม่ได้มี “ทักษะ” มากเท่าที่ตัวเองคิดก็เท่ากับว่าตนเอง “ไม่มีความรู้” ซึ่งมันไม่ใช่ เช่น “แก้โจทย์ปัญหาโดยใช้ TDD ไม่คล่อง” ไม่ได้หมายความว่า “ไม่รู้จัก TDD” เป็นต้น

ทำโจทย์ใหม่อีกรอบ

เมื่อ set expectation เรียบร้อยก็พาทำโจทย์เดิมด้วย TDD ดู ซึ่งระหว่างพาทำนั้นก็จะชี้ให้เห็นถึงแก่นของ TDD ที่มันมีมากกว่าแค่ “red-green-refactor” เช่น

amount = 1

  • TDD ช่วยแบ่งปัญหาใหญ่ ๆ ให้เป็นปัญหาย่อย ๆ โดยในช่วงเวลานึงให้ทำหรือแก้ไขปัญหาเพียงอย่างเดียว
  • TDD ช่วยให้เราได้ส่วนงานเล็ก ๆ ที่เสร็จ (test ผ่าน) โดย code ยังคงเรียบง่าย

amount = 2

  • หลังจากเขียน code เพียงพอให้ test ผ่าน ให้คิดให้ถี่ถ้วนก่อนมองข้ามการ refactoring ไป

amount = 3

  • รับฟัง feedback จาก test อย่างสม่ำเสมอ เมื่อเราเริ่มเขียน test ที่มีโครงเหมือนเดิมซ้ำ ๆ ให้เราทำการ refactor test เช่น ใช้ท่า parameterized test เป็นต้น
  • หากเรารู้อยู่แล้วว่าเราจะเขียน code แก้ปัญหาถัด ๆ ไปอย่างไร ก็ลงมือทำเลย ไม่จำเป็นต้องทำตามกฎ 3 ข้อเสมอไปหากมันทำให้เราทำงานช้าลง

amount = 13

  • หากเรารู้อยู่แล้วว่าเราจะเขียน code แก้ปัญหาถัด ๆ ไปอย่างไร ก็ลงมือทำเลย ไม่จำเป็นต้องเขียนให้ครบทุก case ยิบย่อย
  • TDD จะส่ง feedback อย่างต่อเนื่องเพื่อเพิ่มความรู้และความเข้าใจใน code ของเรามากขึ้นเรื่อย ๆ

amount = 21

  • ถ้า refactoring แล้วไปผิดทาง ก็ถอยกลับได้ง่าย โดยไม่ต้องจมอยู่กับปัญหานานเกินไป
  • สามารถ refactor code โดยมี test การันตีว่า behavior ของ code จะยังคงเหมือนเดิม

พอทำไปได้สักพักจนทุกคนเห็นภาพ (อันนี้ต้องสังเกตสีหน้าคนเรียนกันนิดนึง) ก็ให้เวลาตกตะกอนความคิดแล้วเล่าให้เพื่อนฟังว่ารู้สึกอย่างไรหลังจากใช้ TDD แก้ปัญหา ผลที่ได้คือ

I found it beneficial to begin with simpler tasks before tackling larger problems. This approach helps in reducing the amount of testing at each level, which in turn saves time and makes the process more efficient.

I discovered that starting with simple concepts and building on them gradually is more effective than trying to anticipate everything in advance. Avoiding redundant tests and not hesitating to discard and restart if something doesn’t look right were also key insights for me.

I learned the importance of organizing my work before starting to write code. By proceeding step-by-step and testing continuously, I realized I don’t need to cover everything from the beginning.

TDD ไม่ใช่ศาสนา

หลังจากที่คนเรียนได้ประสบการณ์และเห็นประโยชน์การใช้ TDD แล้วก็ต้องชี้ให้เห็นอีกมุมด้วยว่ามันไม่ได้มาช่วยได้ทุกเรื่อง เช่น

  • ไม่ได้ช่วยให้ระบบเราไม่มี bug
  • ไม่ได้ช่วยทำให้ requirement เล็กลง / deadline สั้นลง
  • ไม่ได้ช่วยทำให้ design ของ code ดีขึ้น

นอกจากนั้นยังแบ่งปันถึงข้อควรระวังอื่น ๆ เช่น

  • อย่าลืมดูว่า unit test ที่เขียนขึ้นมามันผ่านหรือไม่ผ่าน ทั้งที่เขียนใหม่และก่อนหน้านี้
  • ตั้งชื่อ test case ให้เหมาะสมกับบริบท
  • เริ่มจาก unit test case ที่เรียบง่ายที่สุด
  • สังเกตสัญญาณของปัญหาเมื่อ unit test case เริ่มซับซ้อน
  • เขียน code ที่เข้าใจง่าย

ปิดท้ายด้วยการมองขึ้นมาที่ภาพใหญ่

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

  • TDD จะมาช่วยตรงจุดไหน
  • Unit test ที่เกิดจาก TDD จะอยู่กับการทดสอบรูปแบบอื่นได้อย่างไร

Test pyramid https://martinfowler.com/bliki/TestPyramid.html