ทำ Testing เกี่ยวกับเวลาใน JavaScript โดยใช้ Jasmine

mixth
Zarewoft
Published in
2 min readOct 7, 2018
Modified from a photo by Christophe Hautier on Unsplash

หลายๆคนคงเคยเจอกับ requirement ที่เกี่ยวข้องกับเวลากันอยู่เป็นประจำใช่ไหมครับ? ที่ผมเคยเจอมาก็อย่างเช่น

  • ให้ผู้ใช้กรอกวันที่ในการค้นหารายการของวันที่ x ถึงวันที่ y
  • คาดการณ์ว่าผู้ใช้จะได้รับของในวันที่เท่าไร จากวิธีการส่งสินค้า
  • ให้ขึ้นคำเตือนหากตอนนี้เลยเวลาที่กำหนดแล้ว

โจทย์ตัวอย่าง

เพื่อความเข้าใจโค๊ดตรงกัน ในบทความนี้ผมจะยกตัวอย่างการทำงานของโปรแกรมที่ต้องทดสอบว่า

ฟังก์ชั่นชื่อ isExpired(expiryDate: Date): boolean จะคืนค่า

  • true เมื่อ expiryDate น้อยกว่าหรือเท่ากับเวลาปัจจุบัน
  • false เมื่อ expiryDate มีค่ามากกว่าเวลาปัจจุบัน

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

ทำอย่างไร

หลายคนอาจจะเคยเขียนเทสเกี่ยวกับเวลาโดยการสร้าง input เป็น +/- จากเวลาของระบบเลย ซึ่งมันก็เทสได้แต่อาจจะทำให้เทสเคสที่ไม่ครอบคลุมได้ โดยเฉพาะในกรณีที่มันเกี่ยวกับการข้ามวัน ข้าม timezone และอื่นๆ ซึ่งมันอาจจะทำให้เทสเคสไม่เสถียร์ (คือรันทีนึงผ่าน รันอีกทีอ้าววว ไม่ผ่านแล้วหรอ) หรืออาจจะเลวร้ายถึงขั้นที่เราไม่ได้เทส edge cases โดยบังเอิญอีกด้วย

สิ่งที่สำคัญมากๆคือเราอาจจะไม่มั่นใจว่าการเขียนเทสมันจะช่วยให้เราเจอข้อผิดพลาดก่อนไหม เพราะเราต้องไปรอรันเทสนี้ตอน 23:59, 00:00 หรือวันที่ 29 กุมภาฯของอีก 4 ปีข้างหน้าไหม?

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

เพราะฉะนั้น เราจะใช้วิธีการ mock ค่าเวลาของระบบเลยครับ โดยที่ผมเคยทำจะมี:

  1. มองว่าการได้มาซึ่งเวลาปัจจุบันเป็น dependency ของ unit under test -> เราจะทำ dependency injection ครับ
  2. มองว่า unit under test ต้องจัดการเวลาเองได้ -> ในกรณีนี้ที่เราใช้ Jasmine ซึ่งมีตัวช่วยในการ mock ค่าเวลาอยู่ด้วยครับ

Dependency Injection

วิธีนี้คือเราบอกว่าการจะได้มาซึ่งเวลาในปัจจุบันจะเป็น dependency ไม่ได้ถูกสร้างหรือจัดการใน unit ที่ถูกเทสนี้ครับ

เราจะให้คนอื่นเป็นคนสังเกตและสร้างเวลาปัจจุบันให้ ตัวอย่างด้านล่างผมสร้าง TimeService ขึ้นมา ให้มีฟังก์ชั่น now(): Date เพื่อที่ให้เทสเราสามารถจะ spyOn ได้ แล้วให้มันเป็น dependency ของ unit ที่ถูกเทสครับ

ตัวอย่างการทำ DI

เพียงเท่านี้เราก็สามารถจำลองเวลาเพื่อทดสอบ unit ที่ถูกเทสของเราได้แล้วครับ

Mocking with Jasmine Clock

ใน ​Jamsine มี class อยู่ตัวนึงชื่อ Clock ครับ ตัวนี้ทำให้เราสามารถ stub เวลาในช่วงที่เรารันเทสได้ โดยใช้ 3 ฟังก์ชั่นด้วยกัน

  1. mockDate(initialDate: Date): Date
  2. install()
  3. uninstall()

วิธีใช้งานก็เพียงแค่ install ก่อน เสร็จแล้วส่ง Date object ให้กับ mockDateในเทสเคสที่เราต้องการ mock เวลา และ uninstall หลังใช้งานเสร็จครับ

ตัวอย่างการใช้ Jasmine Clock

โดยเบื้องหลังแล้ว Jasmine จะทำหน้าที่ไปแทนที่ Date ของ JavaScript ด้วย Date ปลอมตัวนึง และเมื่อเรา uninstall มันก็จะแทนที่ Date เดิมกลับมาให้ครับ ลองดู source code ได้ที่นี่ครับ

ท่านี้ทำให้เราสามารถใช้ lib ที่เราชอบ ไม่ว่าจะเป็น luxon หรือ moment ในการสร้างหรือแก้ไขค่าของ date ได้ใน unit ที่ถูกเทสของเราครับ

สรุป

บทความนี้นำเสนอการเทสที่มี date 2 ท่าด้วยกันครับ

  1. DI แล้ว stub ค่าของเวลา ณ ปัจจุบันผ่านการ spyOn เป็นวิธีการที่ตรงไปตรงมาดีครับ
  2. ใช้ Jasmine Clock โดยการเรียก jasmine.clock().mockDate() วิธีการนี้ทำให้แต่ละ unit ที่ถูกเทสมีอิสระในการจัดการ date เอง ผมพบว่ามีประโยชน์เวลาเราใช้ lib ตัวอื่นมาช่วย
  3. อาจจะต้องดู trade-off ของการเลือกใช้สองท่านี้ โดยส่วนตัวคิดว่าท่า Jasmine Clock ทำให้เราต้องจัดเตรียมของในการเทสยุ่งหน่อย และดูไม่สวยเท่ากับท่า DI แต่ได้ประโยชน์ตอนมี lib อื่นๆมาเกี่ยวแล้วเราต้องการให้โค๊ดส่วนนั้นดูไม่สับสน

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

ถ้าใครสนใจเรื่องของ timezone กับการเขียนโปรแกรม แนะนำบทความที่ผมพึ่งเขียนไปสองอันนี้ครับ

Extra

ผมเคยเจอกรณีที่เรายุ่งเกี่ยวกับ timezone แล้วทำให้เทสเราไม่เสถียร ขึ้นกับเครื่องที่รันด้วย โดยมีเหตุผลมาจากว่าเครื่องที่รันแต่ละเครื่องมี timezone ไม่เหมือนกัน ทำให้ตอนเราสร้าง date ขึ้นมาแล้วพยายามดึงเฉพาะค่าวัน-เวลาบางตัวมามันได้ค่าไม่เท่ากันครับ

แน่นอนว่าวิธีแก้ในโค๊ดคือการเขียนโปรแกรมในสร้างหรือปรับค่าให้ใช้ timezone ที่ตั้งไว้เท่านั้น แต่ว่าเราก็ต้องมีเทสเคสเพื่อทดสอบว่าการตั้ง timezone ไว้เนี่ย มันช่วยแก้ไขเหตุการณ์นี้ได้จริงๆใช่ไหม

ผมเลยใช้ท่าประมาณเดียวกันกับที่ Jasmine ทำตอน Clock.install() คือเราเก็บค่าของ getTimezoneOffset ซึ่งเป็นฟังก์ชั่นที่บอกว่า timezone ของผู้ใช้ห่างจาก UTC เท่าไรไว้ และแทนที่ด้วย mock function ที่คืนค่าที่เราต้องการลงไปครับ

ตัวอย่างการ mock getTimezoneOffset ให้ timezone เป็น UTC

ใช่เลยว่ามันไม่ค่อย ideal เท่าไร หากใครมีท่าไหนที่เคยทำแนะนำด้วยนะครับ :D

--

--

mixth
Zarewoft

🇹🇭 BKK based 👨‍💻 software engineer 🐈 love cats 🌟 push for more democratic and more transparent gov 🍿 watch way too much movies and tv series