มาหัดใช้ Network Requests Interception บน Cypress ตั้งแต่ Zero จนเป็น Hero
สวัสดีครับ ใน Cypress.io Thailand Group มีหลายคนมักจะเจอปัญหาว่า Test ที่ตัวเองเขียนนั้นรันเร็วเกินไป ไม่ทันกับ data ที่เว็บกำลังดึงข้อมูลมาแสดงผล ทำให้เวลารันเทสนั้นแล้ว Failed เพราะมองหา data ที่ตัวเอง Expect ไว้ไม่เจอ ทำให้เกิด Test Flakiness ตามมา(คือรันเทสผ่านบ้าง ไม่ผ่านบ้างโดยที่ไม่ได้แก้โค้ดใดๆ)
วันนี้ผมจะขอมาแนะนำ Feature เด็ดบน Cypress ที่จะช่วยแก้ปัญหานี้ได้ในระยะยาวแบบที่ว่าเมื่อคุณอ่านบทความนี้จบแล้วสามารถนำไปปรับใช้แล้วเทสของคุณก็จะ Stable ขึ้นอย่างทันตา
Feature ที่ว่านั้นก็คือการทำ “Network Request Interception” นั่นเอง ซึ่งการทำ Network Request Interception นั้นเป็นหนึ่งใน Best Practices ของ Cypress ที่แนะนำให้ทุกคนนำไปใช้ เพราะมันจะช่วยให้คุณสามารถ Synchronize Test และ Network Request ต่างๆ ที่ตัว Web Application ส่งออกไปหา Backend Servers ได้
Network Request คืออะไร?
หลายคนที่อาจจะไม่เคยพัฒนาเว็บมาก่อนอาจจะไม่รู้ว่า ปกติแล้วพวก data ที่เว็บนำมาแสดงผลให้เราดูนั้น ส่วนใหญ่เกิดจากการที่ Web App นั้นมีการเรียก Network Request ออกไปหา Server เพื่อทำการ Fetch data กลับมาแสดงผลให้ User ใช้งาน
ซึ่งรูปแบบของ Network Request อาจจะเป็นการทำ REST API call ผ่าน HTTP หรือว่าอาจจะใช้เป็น Web Socket ก็ได้ ซึ่งลักษณะการทำงานของพวก Network Request เหล่านี้เป็นแบบ Asynchronous กล่าวคือเมื่อตัว Web Application ส่ง Request ออกไปแล้วจะไม่ได้รอรับ Response กลับมาทันที แต่จะรอรับ Response ผ่าน Callback ที่ได้สร้างไว้ และเมื่อมี Response Callback กลับมาแล้ว ตัว Web Application ก็จะนำ Response ที่ได้มาทำการแสดงผลให้ User เห็นอีกที
ดังนั้นเมื่อเป็นการเขียน Automated Test แล้ว การที่ Web App ส่ง Request ออกไปแบบ Asynchronous จึงทำให้เกิดปัญหาที่ว่า ในขณะที่ Web App ยังไม่ได้รับ Response Callback กลับมา ตัว Test ก็พยายามที่จะ Check UI Element ต่างๆ ที่ตัวเอง Expect ว่ามันจะแสดงผลแล้ว ซึ่งทำให้เกิด Assertion Failure และเกิด Test Flakiness ตามมา
Cypress เข้าใจปัญหานี้เป็นอย่างดี จึงได้ทำการพัฒนา API สำหรับทำ Network Request Interception ขึ้นมาเพื่อแก้ปัญหานี้โดยเฉพาะ
รู้จัก Cypress Network Request Interception
เนื่องจาก Cypress รันอยู่บน Run Loop Process เดียวกับ Web Application จึงทำให้ Cypress สามารถ Intercept หรือดักจับทุกๆ Network Request ที่ส่งออกจาก Web App ผ่าน Browsers ออกไปหา Backend Server ได้ทั้งหมด นอกจากที่จะ Intercept Network Request ได้แล้ว Cypress ยังสามารถให้เราควบคุม Behavior หรือสร้าง Stub Response ต่างๆ ได้เองอีกด้วย เพื่อช่วยลดปัญหา Test Flakiness ในกรณีที่ Backend Server ของเราอาจมีการเปลี่ยนแปลงข้อมูลจนเราไม่สามารถกำหนด Expected Result ที่ชัดเจนได้ หรือบางครั้ง Backend Server ของเราอาจจะไม่พร้อมให้ใช้งานในบางเวลา ซึ่งจุดนี้เป็นสิ่งที่เราต้อง Trade-off ว่าเราจะเลือกเขียน Test โดยใช้ Testing Strategy ที่มีความใกล้เคียงกับสิ่งที่ User ใช้งานจริงขนาดไหน?
Cypress Intercept API
โดย Cypress ได้เตรียม API ให้เราสามารถทำ Network Request Interception ได้อย่างง่ายๆ ผ่านการเรียกใช้ cy.intercept()
API โดยในบทความนี้จะขอยกตัวอย่าง Test Scenario ที่นำ cy.intercept()
ไปใช้ทำเทสใน Scenario ที่น่าสนใจเพื่อที่จะเป็นไอเดียให้หลายๆ คนลองนำไปปรับใช้กันดูนะครับ
เริ่มต้น Inspect Network Request บน Web App
ตัวอย่างในบทความนี้ ผมได้พัฒนา Web App ง่ายๆ ขึ้นมาหนึ่งเว็บ ซึ่งเว็บนี้จะทำการ Fetch Data มาจาก Backend Servers ผ่าน REST API call (XHR) และนำ data มาแสดงผล โดยในตัวอย่างนี้จะเป็นการดึงข้อมูลของ Users มาแสดงผลโดยมีรูป, ชื่อ และ Email ของ User แต่ละคนดังตัวอย่างในรูปด้านล่างนี้
ซึ่งเมื่อเราใช้ Chrome DevTools ทำการ Inspect Network Request ดูก็จะเห็นว่ามีการเรียก API call ออกไปแบบนี้ครับ
จะเห็นได้ว่ามีการเรียก API ไปที่ https://reqres.in/api/users?page=1
เป็น HTTP GET
method และเมื่อเรากดไปที่ Tab Response ก็จะเห็น Data แบบนี้
ก็จะเห็นได้ว่า Web App จะนำ JSON ใน Response Body ที่ได้มาทำการแสดงผลในหน้าเว็บนั่นเอง เมื่อเราเข้าใจหลักการแบบนี้แล้ว ทีนี้เราจะมาเริ่มลงมือเขียน Test ด้วย Cypress แบบที่มีการทำ Network Request Interception กันดูครับ
Scenario#1 Cypress Intercepts Network Request and Wait for Response
เราสามารถใช้ cy.intercept()
เพื่อกำหนด Network Request ได้ว่าเราจะ Intercept Request ไหนบ้าง ซึ่งสามารถเขียนได้แบบนี้ครับ
ตรง cy.intercept()
เราสามารถใช้ RouteMatcher
เพื่อกำหนดว่าเราจะ match network request ไหนบ้าง ในกรณีนี้ผมต้องการ intercept API GET /api/users
โดยที่เราสามารถกำหนด path ในลักษณะที่เป็น Glob-like pattern ได้ เช่นในกรณีนี้ผมไม่สนใจว่า query params ของ api นี้จะเป็นอะไร ก็ขอให้ intercept มาทั้งหมด หลังจากนั้นเราสามารถตั้งชื่อเล่น alias
ให้กับ Network Request นี้ได้โดยใช้ .as('<name>')
แบบนี้ เพื่อที่จะให้เราสามารถใช้ reference กลับไปเมื่อเราต้องการจะให้ Cypress มันหยุดรอ Response จาก Request นี้นั่นเอง
และตรง cy.wait()
คือการให้ Cypress หยุดรอ Response ของ Network Request ที่เราได้ Intercept ไว้โดยเราสามารถ reference กลับไปได้โดยใช้เครื่องหมาย @
นำหน้าชื่อ Network Request ที่เราสร้างไว้ได้เลย
ซึ่งในกรณีนี้คือ Request ได้ถูกส่งออกไปหา Backend Server จริงๆ และเมื่อได้ Response Callback จาก Server กลับมาแล้ว ค่อยทำการใช้ Cypress command ทำการ get element และทำ assertion ต่างๆ ต่อไป ซึ่งก็จะทำให้เรา Synchronize การเทสได้ดีขึ้นอย่างมาก เพราะ Data ที่ได้รับกลับมาก็ควรจะถูกแสดงผลบนหน้าเว็บเป็นที่เรียบร้อยแล้ว
เมื่อเราทำการรันเทสข้อนี้ก็จะเห็นว่ามีการ Define Route Matcher ไว้และ Cypress ยังได้บอกจำนวนครั้งที่เกิด Network Interception ให้เราเห็นอีกด้วย และตรง column Stubbed
จะเห็นค่าเป็น No
เพราะ Request และ Response นั้นมาจาก Backend Server เราจริงๆ
Scenario#2 ให้ Cypress ทำการ Stub Response จาก Server ทั้งหมดเพื่อ Control API Behavior
หลายครั้งที่เราอาจจะเคยเจอปัญหาว่าเราไม่สามารถเรียก API เดิมซ้ำๆ ได้ เช่นการเทสระบบลงทะเบียนต่างๆ ถ้ามีการเรียก Register API ซ้ำๆ ตัว Backend Server ก็อาจจะ Return Error กลับมาได้ (เพราะอาจจะห้ามมีการ Register User เดิมซ้ำๆ กัน) ดังนั้นการใช้วิธีการ Stub Network Response ก็เป็นอีกหนึ่งวิธีที่จะทำให้เราสามารถควบคุม Behavior ของ API ที่ Web App เราส่งออกไป เพื่อที่จะทำให้ Automated Test ของเราสามารถถูกนำไปใช้เทสได้ตลอดกาล
เราสามารถ Stub Network Response โดยใช้ cy.intercept()
ได้เลยเช่นกัน สามารถเขียนได้แบบนี้
ในตัวอย่างนี้ผมได้ทำการ define Router Matcher ไว้เหมือนเดิม แต่ใน argument ที่ 2 ของ cy.intercept()
ให้เราทำการใส่ Stub Response เข้าไป โดยในที่นี้ผมต้องการให้ API มัน Return statusCode
200
และมี Response Body ตามที่ define ไว้ใน fixture
file ที่อยู่ใน folder cypress/fixtures/users.json
แบบนี้ครับ
เมื่อรันเทสข้อนี้ก็จะเห็นว่า data ที่เว็บนำมาแสดงผลนั้นเป็น data ที่เราได้ทำการ Stub ไว้ทั้งหมดแล้ว และเมื่อเราสังเกตดูใน Cypress Test Runner ก็จะเห็นตรง column Stubbed แสดงเป็นคำว่า Yes
อยู่นั่นเอง
Scenario#3 Cypress ทำการ Stub บางส่วนของ Response Message
บางครั้งเราอาจจะไม่อยาก Stub Response Message ทั้งหมด แต่เราอาจจะแค่อยากจะแก้ไขมันบางส่วน ก็สามารถทำได้เช่นกัน โดยใน cy.intercept()
เราสามารถใส่ callback before:response
เพื่อทำการแก้ไข Response Message บางส่วน (หรือจริงๆ จะแก้หมดเลยก็ได้) ก่อนที่จะ callback จะถูกเรียกกลับไปหา Web App ของเรา
ในตัวอย่างนี้ ผมได้ทำการ bind callback before:response
ไว้เพื่อทำการแก้ไข data บางส่วนที่อยู่ใน Response Body เป็น User ที่ผมได้ Stub ขึ้นมาเอง ดังนั้นเมื่อรันเทสออกมาก็จะได้หน้าตาของเว็บออกมาแบบนี้
Scenario#4 จำลองการเกิด Network Request Error
ในบางครั้งเราก็อยากจะจำลองสถานการณ์ว่า ถ้าเกิดมีคนไปเตะปลั๊ก Backend Server เราดับไปแล้ว Web App เราจะ Handle Error พวกนี้ยังไง? เราสามารถใช้ cy.intercept()
เพื่อจำลองสถานการณ์ที่เกิด Network Error นี้ได้เช่นกัน โดยสามารถเขียนได้แบบนี้ครับ
ซึ่งในกรณีนี้โค้ดที่ผมเขียนไว้ได้ handle ไว้ว่าถ้าเกิด Network Error ขึ้นให้แสดงผล Popup Modal แจ้งให้ User เห็นแบบนี้ครับ
จะเห็นได้ว่าเราสามารถจำลองสถานการณ์หลากหลายแบบโดยใช้ cy.intercept() เพียง API เดียวเลย ยังมี Scenario อื่นๆ ที่สามารถทำได้อีก ยกตัวอย่างเช่น การ Modify Request ก่อนส่งไปหา Server, การทำ Delay หรือ Throttle Network Bandwidth ในกรณีที่ใช้งานบน Mobile Network ต่างๆ ได้ ซึ่งสามารถเข้าไปดูตัวอย่างเต็มๆ ได้จาก Git Repository ที่ผมสร้างไว้ท้ายบทความนี้ได้เลยครับ
นอกจากที่ได้กล่าวมาทั้งหมดนี้ ยังมีการเรียกใช้ cy.intercept()
ในอีกหลากหลายรูปแบบซึ่งสามารถเข้าไปอ่าน Document เพิ่มเติมได้ที่ลิ้งค์ด้านล่างนี้เลยครับ ทีมงาน Cypress ได้เขียนไว้พร้อมยกตัวอย่างประกอบได้อย่างละเอียดดีมากๆ เลยครับ
หวังว่าบทความนี้จะมีประโยชน์กับหลายๆ คนที่กำลังใช้ Cypress เขียน Test ให้กับ Web App ของคุณอยู่นะครับ ก็อย่าลืมนำเทคนิค Network Request Interception ไปใช้เพื่อทำให้เทสของคุณ Stable ขึ้นในระยะยาวกันนะครับ Happy Testing!