รู้จัก Stub & Spy ตัวช่วยที่จะมาเติมเต็ม ให้ Test ของคุณดีขึ้นใน Cypress.io
สวัสดีครับ วันนี้ผมจะมาพูดถึงอีกหนึ่ง 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 เป็นต้น
ผมว่าหลายๆ คงคงเคยเจอ 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 ที่ถูกต้องหรือไม่ด้วยครับ
Example#2: Stubbing Window Prompt
อีกตัวอย่างนึงแบบง่ายๆ ก็คือการ Stub ฟังก์ชันwindow.prompt()
ซึ่งปกติแล้ว User จะต้องมีการ Input ไปบน Browser ตามที่ Web App ร้องขอ ซึ่งถ้าไม่ Stub ก็เขียนเทสยากอีกเช่นกันครับ
เมื่อมาถึงการเขียนเทส เราก็แค่ 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 เป็นไปตามที่ควรจะเป็นหรือไม่
โค้ดตัวอย่างนี้ 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 ครับ
และนี่ก็เป็นเพียง 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!