Traitanit
Traitanit
Aug 3 · 4 min read

สวัสดีครับ วันนี้ผมจะมาพูดถึงอีกหนึ่ง Feature ที่น่าสนใจของ Cypress.io ที่จะมาช่วยให้การเขียนเทสของคุณดียิ่งขึ้น ง่ายยิ่งขึ้น และ Flaky Test น้อยลง ผมกำลังพูดถึง Stub & Spy ซึ่งเป็น Feature ที่จะทำให้เราทำเทส Scenario ที่หลากหลายมากยิ่งขึ้น สามารถเขียนเทสในเคสที่ไม่เคยคิดว่าจะเขียนเป็น Automated Test ได้ แต่ด้วย Cypress.io นั้นช่วยให้ Test Case เหล่านั้นเป็นจริงขึ้นมาได้ครับ

Stub & Spy คืออะไร ต่างกันยังไง?

ก่อนอื่นเลยขอพูดถึงนิยามความหมายของสองคำนี้ก่อนครับ สำหรับใครที่เคยเขียน Unit Tests มาเยอะๆ ก็คงเคยพอได้ยินหรือเคยใช้สองสิ่งนี้มาบ้างใช่ไหมครับ แต่สำหรับคนที่เคยเขียนมาแต่ End-to-End Tests สองคำนี้คงจะเป็น Concept ที่ไม่ค่อยคุ้นหูมากนัก

Stub

Stub ในเชิง Computer Science นั้นคือโค้ดที่เราเขียนเพื่อจำลอง Behavior ของโปรแกรมหรือโค้ดตัวจริง (เช่น Function, Module ต่างๆ ) เพื่อใช้สำหรับทำเทสโดยเฉพาะ ให้ผลลัพธ์ด้วยค่าที่กำหนดไว้ (Canned Answers) โดย Stub จะถูกเรียกใช้แทนโค้ดจริงเพื่อควบคุม Behavior ของแอพให้เป็นไปอย่างที่เราต้องการ เพื่อลด Dependencies ต่างๆ (ที่เราควบคุมไม่ได้) ที่อาจจะเกิดขึ้น และทำให้เกิด Flaky Test ตามมาภายหลัง

Stub ส่วนมากถูกใช้สำหรับทำ Unit Test แต่ก็สามารถนำมาใช้สำหรับการทำ End-to-End Test บน Cypress.io ได้ด้วยเช่นกันครับ

Spy

ไม่เกี่ยวกับสายลับ 007 แต่อย่างใดครับ​ฮ่าๆ :) Spy คล้ายๆ กับ Stub แต่ว่า Spy นั้นจะไม่มีการเปลี่ยนแปลงหรือควบคุม Behavior การทำงานของโค้ดตัวจริงแต่อย่างใด แต่จะคอยแฝงตัวและ Record&Capture การเรียกใช้งานโค้ดตัวจริงนั้นและทำให้ Test นั้นได้รู้ว่าการเรียกใช้โค้ดส่วนนั้นถูกต้องหรือไม่ เช่น มีส่งผ่านค่า Parameters ครบตามที่ตกลงกันไว้มั้ย, ฟังก์ชันถูกเรียกเป็นจำนวนครั้งที่ควรจะเป็นหรือไม่ เป็นต้นครับ

Stub & Spy บน Cypress.io

เราสามารถสร้าง Stub และ Spy ได้ง่ายๆ บน Cypress.io ด้วยการใช้คำสั่ง cy.stub และ cy.spy ครับ ซึ่งในบทความนี้ผมจะขอยกตัวอย่างง่ายๆ 2–3 ตัวอย่างเพื่อให้เห็นภาพชัดขึ้นว่ามันเอาไปใช้ทำอะไรได้บ้างในการใช้งานจริงครับ

// to define stub
cy.stub(object, 'functionName', 'stubFunction')
// to define spy
cy.spy(object, 'functionName')

Cypress นั้น Derived การทำ Stub และ Spy มาจาก Sinon.js ครับ เพราะฉะนั้นเราสามารถเปิด Document ของ Sinon.js เพื่อดูวิธีการใช้งานทั้งหมดเอามาใช้กับ Cypress ได้เลยครับ ลองดูจากลิ้งค์ด้านล่างนี้ก็ได้ครับ

Sinon.js Stub: https://sinonjs.org/releases/latest/stubs/
Sinon.js Spy: https://sinonjs.org/releases/latest/spies/

Example #1: Prevent External URL Navigation

บ่อยครั้งที่เราอาจจะเคยเจอสถานการณ์ที่ Web App ของเรานั้นต้องมีการ Navigate ไปยัง External URL ต่างๆ ไม่ว่าจะเป็น Web หรือ Custom URL Scheme เช่น tel:// mailto:// หรือจะเป็นการ Deeplink เข้าไปยัง Native Mobile App เป็นต้น

Click เพื่อ Call ผ่าน Facetime บน macOS (tel://)
เมื่อรันเทสก็จะพัง เพราะ Navigate to External URL Scheme ไม่ได้

ผมว่าหลายๆ คงคงเคยเจอ Web ที่พอเรากดปุ่มปุ๊บมันจะให้เรา Navigate ไปหา External App ต่างๆ เช่น Phone Call, E-mail ซึ่งเวลาเขียนเทสนั้นจะยากมากครับ เพราะเราไม่สามารถ Control การทำ Navigation นี้และอีกทั้ง Cypress.io ก็ไม่สามารถ Load External URL Scheme แบบนี้ได้ครับ (ซึ่งจริงๆ แล้วก็ควรจะเป็นแบบนั้นถูกต้องแล้วครับ เพราะเรากำลังเทสแอพของเรา ไม่ได้จะเทสการ Navigate URL ของ Browser) วิธีการเทส Scenario แบบนี้จึงต้องใช้ Stub เข้ามาช่วยครับ

โดยปกติและ Web App จะใช้ window.location.href ในการทำ Navigation ต่างๆ ซึ่ง location.href คือ Property หนึ่งของ Window Object ของ Browser ครับ ซึ่ง Stub นั้นมีข้อจำกัดอยู่ที่ว่า เราไม่สามารถที่จะ Stub Property ได้ครับ เรา Stub ได้แต่ Function ได้เท่านั้น ซึ่งวิธีแก้ก็คือให้เราทำการ wrap Property นี้ไว้เป็น Function แล้ว Expose ฟังก์ชันเพิ่มเข้าไปเป็นอีกหนึ่ง Custom Property ของ Window Object เพื่อให้ Test สามารถ Stub ได้ครับ

หลังจากนั้นเรามาเริ่มเขียนเทสกัน โดยให้เราทำการ Get Window Object ของ App ออกมาผ่าน cy.window() หลังจากนั้นก็ทำการ Stub Function setLocationHref ของ Window object แบบนี้ครับ

ผมทำการ Stub ฟังก์ชันsetLocationHref ด้วยฟังก์ชัน winSetLocationHrefStub แถมยังสามารถ Assert ได้ด้วยว่าแอพเรา Call Function ด้วย Parameter ที่ถูกต้องหรือไม่ด้วยครับ

เมื่อรันเทส Function ถูก Stub ไว้เรียบร้อยและไม่ Navigate ออกไปข้างนอกแล้ว

Example#2: Stubbing Window Prompt

อีกตัวอย่างนึงแบบง่ายๆ ก็คือการ Stub ฟังก์ชันwindow.prompt() ซึ่งปกติแล้ว User จะต้องมีการ Input ไปบน Browser ตามที่ Web App ร้องขอ ซึ่งถ้าไม่ Stub ก็เขียนเทสยากอีกเช่นกันครับ

window.prompt() รอ Input จาก User

เมื่อมาถึงการเขียนเทส เราก็แค่ Stub window.prompt() ไว้แล้ว Return ค่าตามที่เราต้องการ (Canned Answer) และอีกข้อนึงที่ควรรู้คือ Cypress จะทำการ Reset Stub Object ให้เราอัตโนมัติเมื่อเทสรันจบในแต่ละข้อครับ เพราะฉะนั้นเราสามารถสร้าง Stub แยกกันไว้ใน Test Case แต่ละข้อโดยไม่ต้องกลัวว่ามันจะตีกันได้เลยครับ สะดวกมากๆ

เราสามารถทำการ Alias ตัว Stub ของเราโดยใช้คำสั่ง as แล้วตั้งชื่อเป็นอะไรก็ได้เพื่อที่จะนำมา Reference ทำ Assertion ภายหลังได้ด้วยนะครับ

เมื่อรันเทสออกมาก็จะเห็นว่าไม่มี window.prompt() เด้งขึ้นมาให้กวนใจแล้วครับ แถมยังได้ Output เหมือนกับว่ามี User มา Type Text เข้าไปจริงๆ ให้ด้วยครับ

Example#3 Spying Window Confirm Dialog

ตัวอย่างสุดท้ายของบทความนี้จะเป็นการ Spy การเรียกฟังก์ชัน window.confirm() เพื่อเช็คว่าฟังก์ชัน confirm มีการถูกเรียกหนึ่งครั้งและ Confirm Message เป็นไปตามที่ควรจะเป็นหรือไม่

window.confirm() dialog

โค้ดตัวอย่างนี้ Simple มากครับ ถ้า User กด ok ก็จะโชว์ Text Yes, you're hungry แต่ถ้ากด Cancel ก็จะโชว์ No, you're full. ซึ่งใน Test Case นี้ผมต้องการจะเช็คว่าเมื่อกดปุ่มแล้ว App จะต้องมีการเรียกฟังก์ชัน window.confirm() 1 ครั้ง ผมจึงได้ทำการฝัง Spy ไว้ใน Test Case แบบนี้ครับ

เราสามารถใช้ Sinon-Chai ทำ Assertion สำหรับ Spy Object ได้หลากหลายแบบเลยนะครับ อย่างในตัวอย่างนี้ผมใช้ should('be.calledOnce') เพื่อเช็คว่าฟังก์ชันมีการเรียก 1 ครั้ง เป็นต้น สามารถดูตัวอย่างการใช้ Assertion สำหรับ Spy Object แบบอื่นๆ ได้ที่นี่ครับ

เมื่อเริ่มเขียนเทสเราก็จะเห็นว่าผมได้ทำการ Bind Event window:confirm ไว้และให้ Return true ซึ่งก็คือเหมือนกับการกดปุ่ม ok ใน Confirm Dialog นั่นเอง และสามารถเช็คจำนวนครั้งที่ฟังก์ชันถูกเรียกได้ผ่าน Spy Object ครับ

Spy window.confirm()

และนี่ก็เป็นเพียง 3 ตัวอย่างแบบง่ายๆ ที่เราสามารถใช้ Stub & Spy เป็นตัวช่วยเพื่อเติมเต็ม Test ของเราให้ดียิ่งขึ้น มี Test Scenario ใหม่ๆ ที่สามารถเขียนเป็น Automated Test ได้แล้ว ยังมี Use Cases อื่นๆ ที่น่าสนใจที่สามารถนำ Stub & Spy ไปประยุกต์ใช้ได้อีกมากมาย สามารถอ่านเพิ่มเติมได้ที่ https://docs.cypress.io/guides/guides/stubs-spies-and-clocks.html เลยครับ

ตัวอย่าง Code ของตัวอย่างทั้งสาม สามารถเข้าไปดูโค้ดเต็มๆ ได้ที่ Github ของผมด้านล่างนี้เลยครับ

ไว้มีโอกาสผมจะมาเขียนบทความเล่าถึง Feature ที่น่าสนใจอื่นๆ บน Cypress.io อีกนะครับ สามารถเข้ามาพูดคุยแลกเปลี่ยนไอเดียในการเขียน Test ด้วย Cypress.io ได้ที่ Cypress.io Thailand Community กันได้นะครับ วันนี้ขอตัวก่อนแล้วครับ Happy Testing!

LINE Developers Thailand

Closing the distance. Our mission is to bring people, information and services closer together

Traitanit

Written by

Traitanit

Software Engineer in Test, Traveler, Blogger, Man Utd Supporter

LINE Developers Thailand

Closing the distance. Our mission is to bring people, information and services closer together

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