Angular の Unit Testing — Part 4 มาลอง Asynchronous Testing กัน
ใครยังไม่ได้อ่าน Part 3 คลิ๊กบทความด้านล่างเข้าไปอ่านได้ก่อนนะครับ
Recap จากตอนที่แล้วๆมา เราพูดถึงในเรื่องของ
- ข้อดีของการเขียนเทส
- อธิบาย describe, beforeEach และ it เบื้องต้น
- เขียนและรันเทสเคสแบบง่ายๆ
- การม็อคค่า(by pass) ด้วย SpyOn
- การม็อค service ด้วย SpyObject
หลังจากพาร์ทที่แล้ว หลายคนอาจจะสงสัยว่าทำไมโค้ดกับเทสเคสของเราถึงกลายร่างไปหน้าตาประหลาดๆแบบนี้ได้ ทำไมในเทสเคสต้องมี async await มี done() ติดมาด้วย มันคืออะไรกันนะ ต้องขอโทษที่พาร์ทที่แล้วผมพากระโดดข้ามมาไกลไปสักหน่อย พาร์ทนี้จะมาอธิบายกันครับ
เราลองมาดูกัน ว่าถ้าหากตัวฟังชั่นของเราเขียนหน้าตาบ้านๆ แบบไม่เป็น async และไม่มีการใส่ await ตรงตอนเรียก testService.getGithubUserDetail() เลย
แล้วก็เขียนเทสเคสหน้าตาบ้านๆเหมือนเทส component ทั่วๆไป
หรือจะลองมาเขียนแบบนี้
แล้วมาลองรันด้วยคำสั่ง ng test ดูกัน
แน่นอนเราจะเห็น FAILED เกิดขึ้น และเมื่อเราลองไล่ดูข้อความที่มันแดงๆก็จะเห็นข้อความว่า ForTestComponent should return True from isPublicRepoGreaterThan function FAILED Expected undefined to be true และ ForTestComponent should return False from isPublicRepoGreaterThan function FAILED Expected undefined to be false
ซูมเข้าไปชัดของแต่ละเทสเคสเลย จะเห็นว่าเทสเคสเรานั้น มีการคาดหวังผลลัพธ์จากฟังก์ชั่นนี้ไว้เป็น True กับ False แต่สิ่งที่มันได้ออกมาเหมือนกันเลยนั่นคือ undefined อ้าว ทำไมเป็น undefined ล่ะ ทั้งๆที่โค้ดเรามันรีเทิร์นได้แค่ boolean นี่นา
คือก่อนหน้าที่เราจะเข้าใจเรื่องนี้ได้ เราจะต้องเข้าใจเรื่อง Synchronous กับ Asynchronous เสียก่อน
อ้าว!!! แล้วไอสองอันนี้มันต่างกันยังไงล่ะ อธิบายง่ายๆสั้นๆ จากความเข้าใจผม
- Synchronous เวลาโค้ดรัน มันต้องรอให้การทำงานจากโค้ดชุดก่อนหน้า หรือโค้ดด้านบนทำงานเสร็จก่อนมันถึงจะลงมาทำข้างล่างได้ อารมณ์เหมือนกับทำงานเรียงกันแบบคอยผลลัพธ์จากโค้ดข้างบน
- Asynchronous เวลามันรันสิ่งที่ทำได้คือในขณะที่รันโค้ดชุดแรกอยู่ มันสามารถรันโค้ดชุดต่อๆไปต่อได้ โดยไม่ต้องรอให้โค้ดชุดบนทำงานเสร็จหรือไม่ต้องรอผลลัพธ์จากโค้ดบน ก็สามารถรันโค้ดข้างล่างต่อไปได้เลย
ซึ่งจากการทำงานของเจ้า Angular เนี่ยมันรันเป็นแบบ Asynchronous เพราะ service getGithubUserDetail มันเป็นการรีเทิร์นค่าแบบ Promise
ตอนท่ีมันไปเรียก this.testService.getGithubUserDetail() ในฟังก์ชั่น isPublicRepoGreaterThan โดยที่ใน service นี้ มันมีการยิง api ออกไปด้านนอกนั้น ตอนนั้นในฟังก์ชั่น isPublicRepoGreaterThan มันก็ทำงานต่อไปทันทีโดยไม่คอยผลลัพธ์จาก service getGithubUserDetail ที่กำลังทำการเรียก api อยู่เลย ทำให้โค้ดบรรทัดต่อไปที่มันไปรันเจอก็คือ return returnValue; ซึ่งก็คือรีเทิร์นผลลัพธ์ที่ยังไม่ถูก assign ค่า ออกจากฟังก์ชั่นนี้ไปเลย
ทำให้โค้ดในส่วนสีแดงนี้ ยังไม่ถูกรันเลยด้วยซ้ำ นั่นเป็นเหตุผลที่ว่าทำไมค่าที่เราได้ในเทสเคสมันจึงเป็น undefined
เราจึงต้องใส่ await ไปที่ตรงส่วนการเรียกใช้ service เพื่อให้ฟังก์ชั่นนี้ก่อนที่มันจะไปรันโค้ดชุดต่อไป ให้มันรอค่าที่ได้จาก service นี้เสียก่อน และอการที่จะใช้ await ในฟังก์ชั่นได้ ฟังก์ชั่นเราต้องเป็นแบบ Asynchronous เสียก่อน เราจึงใส่ async ไปที่หน้าฟังก์ชั่น แล้วจากการที่ฟังก์ชั่นเรากลายเป็น async นั่นส่งผลให้ฟังก์ชั่นอื่นๆที่มาเรียกใช้ฟังก์ชั่นเรา ย่อมต้องเป็น async เหมือนกัน และฟังก์ชั่นเรานั้นจะไม่สามารถรีเทิร์นค่าเป็น boolean แบบธรรมดาๆได้อีกแล้ว เพราะเราจะต้องรีเทิร์นคุณสมบัติการเป็น Asynchronous ไปต่อ เราจะต้องเปลี่ยนเป็น Promise ที่ห่อหุ้ม boolean ไว้นั่นเอง
จริงๆแล้วฟังก์ชั่นที่ใช้ในการเขียนเทสเคสแบบ Asynchronous จะมีอยู่ 3 แบบคือ
- async
- done
- fakeAsync
เราจะมาค่อยๆลองวิธีการใช้งานแต่ละแบบดูกันครับ
async
ตัวนี้จะเป็นฟังก์ชั่นที่เราจะทำให้เทสเคสของเรากลายเป็น Asynchronous นั่นเอง โดยตอนใช้งานก็เหมือนกับในโค้ดเลย คือใส่ async กับ await เข้าไป โดยจะมีฟังก์ชั่นอีกตัวของ Angular ที่พ่วงมาให้เราสำหรับการเทสแบบ Asynchronous ด้วยนั่นก็คือ fixture.whenStable()
แต่เนื่องจากเทสเคสของเราไม่ได้ใช้ฟังก์ whenStable ผมเลยเขียนตัวอย่างการใช้งานไว้ให้ดูข้างล่างแทนครับ
it('test async() and whenStable()', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(____).toBe(______);
});
}));
done
ตัวนี้จะเป็น builtin ของ Jasmine ที่เอาไว้ใช้ในการจัดการเทสเคสที่เป็นแบบ Asynchronous โดยเราจะใส่ค่า done เป็นพารามิเตอร์เข้าไป และไปเรียกใช้ในฟังก์ชั่น ที่มีการเรียกใช้ Asynchronous ฟังก์ชั่นอยู่ โดยการที่เราใส่ done() ไว้ในนั้นหมายถึงว่า เทสเคสนี้จะจบการทำงานได้ก็ต่อเมื่อ โค้ดมันรันไปจนถึงฟังก์ชั่น done นี้แล้ว หรืออีกอย่างนึงก็คือเพื่อ make sure ว่าเทสเคสนี้จะอยู่รอผลจนกว่า Asynchronous ฟังก์ชั่นในเทสเคสนี้จะทำงานจนเสร็จนั่นเอง
fakeAsync
ตัวนี้ก็เอาไว้ทำเทส Asynchronous เหมือนกัน โดยมันจะฟังก์ชั่น tick ที่คล้ายๆฟังก์ชั่น setTimeout เลย โดยเจ้า tick มันจะทำหน้าที่หน่วงเวลาไว้จนกว่า Asynchronous ฟังก์ชั่นจะทำงานเสร็จนั่นเอง
ก่อนใช้งานต้องอิมพอร์ต fakeAsync กับ tick ด้วย
import { fakeAsync, tick } from ‘@angular/core/testing’;
และแน่นอนว่า ถ้าหากเราไม่ได้ใส่ tick() มันจะได้รีเทิร์นค่าเป็น undefined แน่นอน เพราะมันไม่รอให้ isPublicRepoGreaterThan ทำงานเสร็จก่อนที่จะไป expect ผลลัพธ์ข้างล่าง
หลังจากที่เราได้ลองกันมาทั้ง 3 แบบแล้ว คราวนี้การจะนำไปใช้งานจริงก็อยู่ที่ความเหมาะสมของแต่ละคน และของแต่ละงานกันแล้วล่ะครับ ว่าจะเลือกแบบไหนไปใช้
ตอนนี้ขอจบไปเพียงเท่านี้ครับในตอนหน้าจะพาไปลองฝึกเขียนเทสเคสในรูปแบบต่างๆเพื่อเทสฟังก์ชั่นหลายๆรูปแบบกันให้มากขึ้นครับ