สิ่งที่ได้เรียนรู้จากการใช้ Snapshot Testing ใน Jest

aofleejay
aofleejay
Sep 30 · 3 min read

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

Photo by Malcolm Lightbody on Unsplash

Snapshot Testing แบบสั้นๆ

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

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

ลองมาดูการใช้งานกันหน่อย

เราสามารถทำ snapshot ได้ด้วยคำสั่ง toMatchSnapshot ครับ จากนั้น Jest จะสร้างไฟล์นามสกุล .snap ขึ้นมา แล้วเก็บ snapshot ไว้ที่นั่น

จากตัวอย่างด้านล่าง โค้ดเทสอยู่ที่ Twitter.test.js ตัว snapshot ก็จะอยู่ที่ __snapshots__/Twitter.test.js.snap แบบนี้ครับ

(ซ้าย) โค้ดเทส — (ขวา) Snapshot

หากมีการแก้ไขโค้ดที่มากระทบกับ snapshot ของเรา เราก็จะรับรู้ได้ดังนี้ครับ

ผลลัพธ์ของการลบ class และเพิ่ม heading tag

ปล. ไม่จำเป็นเสมอไปว่าจะต้องใช้ snapshot กับ UI เท่านั้นนะครับ เราสามารถใช้ก้บข้อมูลประเภทอื่น ๆ เช่น string, number, object ก็ได้

และต่อไปนี้เป็นสิ่งที่ผมพบเจอระหว่างทางครับ 🚸


ทำให้ Snapshot อยู่ใกล้สายตาเสมอ

ด้วยความที่โค้ดเทสและผลลัพธ์อยู่คนละที่กัน มันก็มีโอกาสสูงที่ไฟล์ snapshot จะถูกดูแลน้อยกว่า อาจจะเพราะไม่ค่อยได้เปิดบ่อย ๆ หรือแย่กว่านั้นก็คือไม่เคยไปเปิดไฟล์ snapshot ดูเลยแม้แต่ครั้งเดียว (สารภาพว่าผมก็เคยทำ 🙏)

ใช่ครับ มันไม่ใช่ข้อเสียอะไรของ Jest snapshot เลย มันเป็นนิสัยที่ไม่ดีของผมในการทำ snpashot มากกว่า 😢

ปัญหานี้จะเกิดประจำเวลารันเทสครั้งแรก คือรันเทสปุ๊บ มันก็จะบันทึก snapshot ปั๊บ โดยไม่ได้แสดงผลลัพธ์ให้เราดู เราเลยต้องตามไปดูในไฟล์ .snap อีกที เพื่อดูว่ามันบันทึกถูกต้องหรือไม่

หลังจากนั้น ผมเลยชอบที่จะใช้ .toMatchInlineSnapshot() แทนการใช้ .toMatchSnapshot() มากกว่าครับ เพราะ .toMatchInlineSnapshot() จะทำการเก็บ snapshot ไว้ในโค้ดของเราแทนการเก็บไว้ในไฟล์ .snap แบบนี้ครับ

น่อววววววว 😎

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


Snapshot ยาวไป ไม่อ่าน

โค้ดที่ยาวเกินไปมันอ่านยากถูกไหมครับ ? snapshot ก็เช่นกัน ถ้ายาวเกินไปก็ก่อให้เกิดผลเสียมากมาย เช่น

  • ใช้แรงในการอ่านเยอะมากกกกกก มากจนถึงขั้นไม่อ่านดีกว่า 😠
  • อ่าน diff โค้ดไม่ไหว อันนี้ผมให้ภาพอธิบายก็แล้วกันครับ
ยาวไปไม่อ่าน กดอัพเดทแม่มเลย 😿

เลวร้ายที่สุดก็จะจบด้วยการอัพเดต snapshot แบบมั่ว ๆ หรือลบ snapshot แล้วสร้างใหม่ทับของเดิมไปเลย 😢

ก่อนจะไปต่อให้ลองนึกเล่น ๆ หน่อยนะครับว่า “snapshot ยาวกี่บรรทัด คุณถึงจะไม่อยากอ่านมันแล้ว ? ”

ติ๊ก ต่อก … ติ๊ก ต่อก … ⏰

พอได้ตัวเลขแล้ว เอาไปใช้ร่วมกับ eslint-plugin-jest นะครับ 😄 คือในนั้นจะมีกฏข้อนึงชื่อว่า no-large-snapshots ครับ คือเราจะกำหนดได้ว่าจะไม่ให้ snapshot ยาวเกินกี่บรรทัด ซึ่งค่าตั้งต้นอยู่ที่ 50 บรรทัดครับ

สมมติว่าผมสามารถอ่าน snapshot ได้มากสุด 20 บรรทัดแค่นั้น เกินกว่านั้นผมอ่านไม่ไหวแล้ว ผมอาจจะตั้งกฏไว้แบบนี้ครับ

"jest/no-large-snapshots": ["error", { "maxSize": 20 }]

แค่นี้ก็แดงเถือกจนไม่กล้า commit แล้วครับ 😝


ใช้เท่าที่จำเป็น

การทำ snapshot UI ทั้งก้อน อาจจะบันทึกทุกจุดของ UI เก็บไว้ก็จริง แต่ก็ต้องแลกมาด้วยการที่เราแก้อะไรนิดหน่อย snapshot ก็พัง หลายครั้งมันก็น่ารำคาญใช่ไหมครับ ?

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

  • ใช้การหาอิลิเมนต์ตรง ๆ แทนการทำ snapshot ทั้งก้อน
expect(component.find('#modal')).toHaveLength(1)
  • ใช้การทดสอบค่า null แทนการเก็บ snapshot ในกรณีไม่ render อะไร
expect(component).toBe(null)

ยิ่งการเทสกับ DOM แล้ว ผมชอบใช้ jest-dom ที่มี matcher ดี ๆ หลายตัวให้เราใช้ ไม่ว่าจะเป็น ดูว่ามี element อยู่ใน document ไหม ? , มี class หรือ attribute ที่ต้องการไหม ?

ซึ่งทำให้เราสามารถทดสอบสิ่งที่จะถูก render โดยไม่ต้องใช้ snapshot ก็ได้

expect(myElement).toBeInTheDocument()
expect(myElement).toHaveAttribute('disabled')
expect(myElement).toHaveStyle('display: none')

วิธีการตรงนี้ไม่ได้ตายตัวนะครับ สิ่งที่ผมจะสื่อก็คือ “มันจำเป็นต้องทำ snapshot จริง ๆ หรือเปล่า” เท่านั้นเองครับ


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

แล้วเจอกันใหม่บทความหน้า ขอให้มีความสุขในการเขียนโค้ดครับ 😎

aofleejay

Written by

aofleejay

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade