Angular の Unit Testing — Part 2 มาลองม็อคค่ากัน

ถ้าใครยังไม่ได้อ่าน Part 1 คลิ๊กบทความด้านล่างเข้าไปอ่านได้ก่อนเลยครับ

Recap จากตอนที่แล้ว เราพูดถึงในเรื่องของ

  • ข้อดีของการเขียนเทส
  • อธิบาย describe, beforeEach และ it เบื้องต้น
  • เขียนและรันเทสเคสแบบง่ายๆ

คราวนี้ถึงคราวที่เราจะมาเริ่มสำหรับพาร์ทนี้กันแล้วครับ โดยตอนนี้จะเริ่มเข้ามาโฟกัสที่การม็อคค่าสำหรับการทำเทส (ม็อคค่าในที่นี้ไม่ใช่กาแฟและก็ไม่ใช่ test framework นะครับ ฮ่าๆๆ แต่เป็นคำที่ทีมผมใช้ในการสื่อสารกันสำหรับการทำ Mockup ต่างๆ ผมเลยติดคำนี้มา)

อันดับแรกเลยเปิดไฟล์ component หลักของเราครับ for-test.component.ts และให้ลบฟังก์ชั่น increaseValue ที่เคยเพิ่มไปเมื่อพาร์ทที่แล้ว ออกก่อนเลย

increaseValue(val: number, inc: number): number {     return val + inc;} //ลบฟังก์ชั่นนี้ออก

แล้วเพิ่มสองฟังก์ชั่นนี้เข้าไป setSomething กับ getRandomValue

setSomething(val: number): string {     let returnValue: string;     if (val - this.getRandomValue() < 1) {          returnValue = 'Success';     } else {          returnValue = 'Fail';     }     return returnValue;}getRandomValue(): number {     const a = 10;     const c = 20;     return (a + c) * 2;}

อัพเดตไฟล์ for-test.component.ts เสร็จแล้วจะได้หน้าตาแบบข้างล่างนี้

เราจะมี 2 ฟังก์ชั่น

  • setSomething ที่รับพารามิเตอร์ type number และมีการรีเทิร์นค่าจากฟังก์ชั่นเป็น string โดยการทำงานของฟังก์ชั่นนี้คือ จะทำการส่งค่าตัวเลขเข้าไปทำอะไรสักอย่างแล้วคอยรับผลว่า Success หรือ Fail โดยในฟังก์ชั่นนี้จะมีการเรียกใช้ฟังก์ชั่นอื่นมาด้วย ก็คือฟังก์ชั่น getRandomValue นั่นเอง
  • getRandomValue เป็นฟังก์ชั่นที่รีเทิร์นค่าตัวเลข(สักค่านึง) ออกมาจากฟังก์ชั่น

จะเห็นได้ว่าการทำงานของทั้งสองฟังก์ชั่นนี้ถูกร้อยกันอยู่ด้วยการเรียกใช้ ฟังก์ชั่น getRandomValue ในฟังก์ชั่น setSomething และการมีเงื่อนไขในการตัดสินใจอะไรบางอย่าง ซึ่งในมุมของผมหากเราต้องการที่จะเขียน unit test เพื่อเทสฟังก์ชั่นนี้ เราจะต้องแบ่งการเทสนี้เป็น 2 เทสเคส นั่นก็คือ เคสที่รันแล้วได้ค่ารีเทิร์นเป็น ‘Success’ และ ‘Fail’

ใครที่ยังเก็บเทสเคส should increse value from 10 to 25 ที่เขียนไว้เมื่อพาร์ทที่แล้วอยู่ก็อย่าลืมไปลบออกด้วยนะครับ

it('should increse value from 10 to 25', () => {     expect(component.increaseValue(10, 15)).toBe(25);}); //ลบเทสเคสนี้ออกนะ

แล้วเพิ่มเทสเคสใหม่เข้าไป 2 เทสเคส ตามค่าที่เราอยากได้จากฟังก์ชั่นนี้

it('should return Success from setSomething function', () => {     expect(component.setSomething(10)).toBe('Success');});it('should return Fail from setSomething function', () => {     expect(component.setSomething(20)).toBe('Fail');});

เขียนเสร็จแล้วจะออกมาประมาณนี้

โดยเราจะมี 2 เทสเคสที่แทบจะหน้าตาเดียวกันเลย แต่แค่ชื่อเทสเคส และการคาดหวังผลลัพธ์ที่ต่างกัน

เรามาลองรันเทสดูก่อนสักรอบ เข้าไปที่โฟลเดอร์โปรเจ็คของเรา แล้วพิมพ์ ng test

ผลที่ได้ออกมาจะเป็นแบบนี้

Executed 6 of 6 (1 FAILED) (0 secs / 0.174 secs)
ForTestComponent should return Fail from setSomething function FAILED Expected 'Success' to be 'Fail'. at UserContext.<anonymous> src/app/components/for-test/for-test.component.spec.ts:31:40)

จะเห็นได้ว่า เรามีเทสเคสทั้งหมด 6 เทสเคส(มีในไฟล์ app.component.spec.ts 3 เทสเคสและ ไฟล์ for-test.component.spec.ts 3 เทสเคส) และมันมี FAILED อยู่ 1 คือเทสเคสที่ชื่อว่า ‘should return Fail from setSomething function’ โดยเทสเคสนี้เรามีความคาดหวังว่ามันจะรีเทิร์นค่า Fail ออกจากฟังก์ชั่น setSomething แต่มันดันออกมาเป็น Success เนาะ

ซึ่งแน่นอนอยู่แล้วล่ะว่าเทสเคสที่เราเพิ่มไปทั้ง 2 อันนั้นเราใส่พารามิเตอร์ที่เหมือนกันไปทั้ง 2 เคส component.setSomething(10) ซึ่งหลายคนที่อ่านมาถึงตรงนี้ก็คงจะเอะใจแล้ว ว่าทำไมไม่แก้ค่า 20 ในเทสเคสที่มัน FAILED ให้เป็นค่าอื่นเพื่อให้มันเข้า else ในฟังก์ชั่น setSomething เพื่อให้มันเซ็ทค่า returnValue = ‘Fail’ ล่ะ

คำตอบคือเราจะไม่ทำอย่างนั้นครับ เพราะถ้าเราทำแบบนั้น มันจะเป็นการหลอกเทสเคสให้มันรันผ่านๆไป โดยไม่สนใจความถูกต้องของฟังก์ชั่นจริงๆซะมากกว่า และเนื่องจากสิ่งที่เราต้องการคือความถูกต้องของการทำงานในฟังก์ชั่น setSomething โดยไม่สนใจค่าจากฟังก์ชั่นอื่นที่เข้ามาเกี่ยวข้องเนาะ และอันนี้คือเรารู้เราเห็นอยู่แล้วว่าค่าที่มันรีเทิร์นออกมาจากฟังก์ชั่น getRandomValue มันคือค่า 60 เป็นค่าคงที่ แล้วถ้าในความเป็นจริงเราไม่รู้ล่ะว่าสิ่งที่มันจะรีเทิร์นออกมาจากฟังก์ชั่นที่เรียกใช้คืออะไร หรือค่าที่มันรีเทิร์นออกมานั้นเปลี่ยนแปลงไปเรื่อยๆ เช่นครั้งแรกรีเทิร์นค่าออกมาเป็น 60 ครั้งที่สองเป็น 40 ครั้งที่สามเป็น 120 ถ้าเป็นแบบนี้เราไม่ต้องไปนั่งแก้เทสเคสเพื่อให้มันปรับค่าตามการรันของทุกครั้งเหรอ !!!

จริงๆแล้วถ้าเราทำ TDD ปัญหาเรื่องการคิดตรงนี้น่าจะไม่เกิดนะ

ซึ่งผมอยากจะแนะนำให้เข้าไปอ่าน Principles of unit testing ในบทความนี้ครับ แล้วจะเข้าใจเหตุผลในการเขียนเทสที่ดีมากขึ้น


เพื่อแก้ปัญหาตรงนี้เราจะม็อคค่าให้กับฟังก์ชั่น getRandomValue เพื่อให้มีการรีเทิร์นค่าที่อยู่ในการควบคุมของเราด้วย Spy

SpyOn ที่จะทำนี้เป็นฟีเจอร์ของ Jasmine Framework ที่เรากำลังเขียนเทสอยู่นั่นเอง โดย Spy จะทำหน้าที่ม็อคค่ารีเทิร์นจากคลาส หรือฟังก์ชั่นที่เราเขียนไว้อยู่แล้ว นั่นก็คือเราจะสามารถใช้ม็อคค่าให้กับคลาส ForTestComponent ได้ และสามารถม็อคค่าให้กับฟังก์ชั่น setSomething และ getRandomValue หรือจะเป็นฟังก์ชั่นใดๆในคลาสนี้ก็ได้

เราจะเขียน Spy เพื่อม็อคค่าออกมาได้ประมาณนี้

spyOn(component, 'getRandomValue').and.returnValue(10);

ผมจะอธิบายตรงโค้ดส่วน Spy ที่เราเพิ่มเข้าไปในแต่ละเทสเคสนะครับ คือฟังก์ชั่น spyOn ของ Jasmine จะรับพารามิเตอร์ 2 ตัว ตัวแรกจะเป็น component ที่เราจำลองมาจาก Tesbed หรืออาจจะเป็น component จริงๆเลยก็ได้ (ซึ่งจะพูดถึงในบทความตอนถัดๆไปครับ) และตัวที่สองจะเป็นชื่อฟังก์ชั่นที่รับค่าเป็นชื่อฟังก์ชั่น ที่ดูเหมือนจะรับค่าเป็น type string แต่จริงๆมันไม่ใช่ string แต่มันเป็น keyof นะครับ และตามด้วย .and ความหมายก็ตรงๆตัวเลย และตามด้วยฟังก์ชั่น returnValue ตรงนี้เราจะใส่พารามิเตอร์ที่เราจะม็อคค่ารีเทิร์นให้กับฟังก์ชั่นนี้ ซึ่งในนี้เราใส่เป็น 10 เข้าไป

ซึ่งจากโค้ดตรงนี้พูดง่ายๆเลยคือ ถ้ารันเทสแล้วเจอฟังก์ชั่นชื่อ getRandomValue ให้ทำการ by pass ค่าที่รีเทิร์นกลับมาจากใน getRandomValue ทันทีเลย

คราวนี้เรามาลองรันเทสด้วยคำสั่ง ng test กันเลย

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

โอเคครับ เอาเป็นว่าสำหรับพาร์ทนี้ขอจบไปแค่ตรงนี้ครับ ในครั้งหน้าเนี่ย ผมจะมาแนะนำวิธีการม็อคค่าด้วย SpyObject กันครับ

odds.team

Odds Team

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