ตามคำนิยามแล้ว Unit Testing คือ “วิธีการทดสอบ Software ที่ใช้ทดสอบส่วนที่เล็กที่สุดของ Code เพื่อแสดงให้เห็นว่ามันควรจะทำงานได้อย่างที่ควรจะเป็น โดยโปรแกรมเมอร์” ซึ่งใน Nest.js นี้ เราจะทำการทดสอบในส่วน Method ของแต่ละ Class โดยใช้ framework ที่ชื่อว่า Jest
หลักการเขียน Unit Testing คือ
- เทสต์เคสที่เขียนต้องไม่ต่อกับ Database จริง
- เทสต์เคสที่เขียนต้องไม่เรียกใช้ Service ภายนอก
ด้วยเหตุนี้เราจึงจะใช้การ Mock หรือการจำลองผลลัพธ์ของการเรียกใช้ Database และ Service ขึ้นมา เพื่อให้ Method ที่เรากำลังเขียนเทสใช้ค่าพวกนั้นแทนการเรียกใช้ Database และ Service ภายนอก
การเขียน Unit test สำหรับ function ทั่วไป
ตัวอย่าง method ที่จะเขียนเทส
หลักการของ Method createUser นี้คือ การตรวจเช็คก่อนว่าใน Database ของเรามี user ที่จะสร้างนี้อยู่แล้วหรือไม่ โดยส่ง name ของ userInfo เข้าไปใน method findOne ถ้ามีอยู่แล้วจะทำการ return ข้อมูล user ของคนนั้นออกมาให้ แต่ถ้าไม่มี จะทำการ save ข้อมูลของ userInfo นี้ลงไปใน Database
ตัวอย่างของการเขียน Mock Entity แบบง่ายๆ
โดยตัวอย่างข้างต้นเป็นการจำลองฟังก์ชั่น findOne, save
ตัวอย่างการเขียน Unit Testing ของ Method ‘createUser’
ในส่วนแรกของตัวอย่าง จะเป็นการ Configuration สำหรับ Unit Testing โดย beforeEach (คือ method ที่ไว้กำหนดสิ่งที่จะต้องทำ ก่อนที่จะไปทำแต่ละ test case) จะกำหนดค่าต่างๆ ไว้ ตัวอย่างเช่น
ซึ่งเป็นการกำหนด mock class ของ Entity ของเรา โดยใช้ class mockUsersEntity แทน
ต่อไปคือส่วนที่เป็นการเขียน unit testing
จากตัวอย่างของ method createUser จะเห็นว่าเราต้องส่ง parameter เข้าไป 1 ตัว คือ userInfo เราจึงจำลองค่า mockUser และค่าต่างๆที่จำเป็นขึ้น
เมื่อจำลองค่าขึ้นมาเรียบร้อย เราก็จะมาจำลองการการเรียกใช้งาน mock Entity ของเราในแต่ละ method
การจำลองค่าที่ส่งคืนกลับมา จะใช้ฟังก์ชั่น mockResolvedValue() และกำหนดส่งค่าที่ต้องการให้ส่งกลับมาเข้าไป โดยเราจำลองว่า ถ้าเรียก findOne ให้คืนค่ามาเป็น undefined เพื่อจำลองว่า ยังไม่มี user ที่ชื่อ June อยู่ใน Database เมื่อเรียก save ก็ให้คืนค่าเป็น mockUser ที่เรากำหนดไว้
ส่วนบรรทัดนี้
เป็นการเรียกใช้ createUser ของเราแล้วนำผลลัพธ์ที่ได้มาเก็บไว้ในตัวแปรที่ชื่อว่า resultTestCreateUser
จากนั้นเราจะตรวจสอบว่า ค่าที่ได้จากการทดสอบตรงกับค่าที่เรากำหนดหรือไม่
โดยใช้ expect().toStrictEqual() เพื่อเปรียบเทียบว่า 2 ตัวแปรนี้มีค่าต่างๆ เหมือนกันหรือไม่ ถ้าเหมือนกันจะผ่านไปทำฟังก์ชั่นถัดไป ถ้าไม่เหมือนกัน จะแจ้ง error ว่าทั้ง 2 ตัวแปรนี้แตกต่างกันอย่างไรบ้าง
ในส่วนของ .toBeCalledWith() นั้น เป็นการตรวจสอบพฤติกรรมภายใน method ว่าเจ้าฟังก์ชั่น mockUsersEntity.findOne ที่เราสร้างไว้เนี่ย มันถูกเรียกด้วย parameter { where: { name: mockUserInfo.name }} หรือไม่
และ mockUsersEntity.save ถูกเรียกด้วย parameter userInfo เหมือนที่เราได้ส่ง parameter เข้าไปใน userService.createUser หรือไม่
การใช้ spyOn() เพื่อ mock function
ในกรณีที่ method ที่เราทำการเทสมีการเรียกใช้ method อื่นใน class เดียวกัน เราสามารถใช้ jest.spyOn() เพื่อทำการจำลองค่าที่จะคืนกลับมาได้ เช่น
จะเป็นการจำลองว่า ถ้าเรียกใช้ method create ใน class userService จะคืนค่าเป็น result ตามที่เรากำหนดไว้
การตรวจสอบ Exception
ในกรณีที่ต้องการที่จะทดสอบการ throw exception จากเดิม เราสามารถเปลี่ยนวิธีการเขียนจากรูปแบบเดิม เช่น
เป็น
โดยเมื่อ method createUser มีการ throw exception ออกมา จะเข้าตรง catch จากนั้นเราจะตรวจสอบว่า exception ที่ throw ออกมานั้นเป็นตามที่เรากำหนดไหม โดยในตัวอย่างจะต้องเป็น NotFoundException
จากนั้นทำการสร้างตัวแปร exception ขึ้นมา เพื่อนำมาตรวจว่า error message เป็นเหมือน expectedResult ที่เรากำหนดไว้หรือไม่
การ Mock Library
ในกรณีที่เราต้องเขียน unit test ของ method ที่มีการใช้ library ที่ import เข้ามา เราต้อง mock library ขึ้นมา เนื่องจากเราไม่สามารถ mock class ในนั้นได้
ตัวอย่าง method ที่ใช้ library “google-auth-library”
ตัวอย่างการ mock library ของ google-auth-library
จากตัวอย่างการ mock library ของ google-auth-library จะเห็นว่าเรา return เป็น object ที่มี OAuth2Client อยู่ข้างใน โดย OAuth2Client นี้จะใช้ mockImplementation() เพื่อ mock function ให้รองรับการใช้แบบ class ในบรรทัดนี้
ถ้าหากไม่ได้ใช้งานแบบ class เราสามารถเปลี่ยนไปใช้ mockResolvedValue หรือ mockReturnValue แทนได้เลย
หลังจากนั้น เรากำหนดให้ OAuth2Client มีการคืนค่าเป็น object ที่มีฟังก์ชั่น verifyIdToken เนื่องจากในโค้ดเราเรียกใช้ verifyIdToken ของ OAuth2Client ดังนี้
โดยเราจะใช้ mockResolvedValue กับ verifyIdToken เพราะมันเป็น asynchronous function และให้คืนค่าเป็น object ที่มีฟังก์ชั่น getPayload ข้างใน ตามการใช้งานของโค้ดบรรทัดนี้
โดยเราจะใช้ mockReturnValue กับ getPayload เพราะมันเป็น synchronous function เพื่อกำหนดค่าที่จะคืนกลับมาเมื่อเรียก getPayload
ตัวอย่างการเขียน unit test ของ method getGoogleProfile
กรณีที่เราต้องการตรวจสอบค่าที่ส่งเข้าไปใน library ที่เรา mock ขึ้นมา เราสามารถเปลี่ยนวิธีการเขียนจาก
เป็น
ทั้งนี้ยังมีข้อจำกัดของการ mock library ที่ควรระลึกถึง ดังนี้
- เราต้องใส่การ mock library ไว้ใต้ import statement ในไฟล์ test ของเรา
- ค่าที่จะ mock หรือตรวจสอบสามารถกำหนดได้เพียงแค่ครั้งเดียวและไม่สามารถเปลี่ยนแปลงค่าในแต่ละ test case ได้