ใครว่า E2E เทสรันช้า เทคนิคง่ายๆ ที่จะทำให้ Cypress รันเร็วติดจรวด
เกริ่นก่อนว่าที่ Pronto Tools เราเปลี่ยนมาใช้ Cypress มาได้ซักพักแล้วครับแล้วเราชอบมันมาก เราเลยไม่ลังเลเลยที่จะย้ายและเพิ่ม E2E เทสมาอยู่บน Cypress ซึ่งพอเทสมันเพิ่มขึ้นอย่างรวดเร็ว เวลาที่ใช้รันมันก็เพิ่มขึ้นอย่างมีนัยยะสำคัญ
เกริ่นก่อนว่าที่ Pronto Tools เราเปลี่ยนมาใช้ Cypress มาได้ซักพักแล้วครับแล้วเราชอบมันมาก เราเลยไม่ลังเลเลยที่จะย้ายและเพิ่ม E2E เทสมาอยู่บน Cypress ซึ่งพอเทสมันเพิ่มขึ้นอย่างรวดเร็ว เวลาที่ใช้รันมันก็เพิ่มขึ้นอย่างมีนัยยะสำคัญ
ทำไม E2E เทสถึงรันช้า 🐢
ปัญหาหลักเลยคือ E2E เทสจะค่อนข้างมี Step ที่ทำงานซ้ำๆ เกิดขึ้นบ่อยๆ เพื่อ Setup ข้อมูลให้พร้อมที่จะเทสใน TestCase หลัก ปัญหาหลักๆ ก็คืองานซ้ำๆ นั้นถ้ามันใช้เวลารันนาน ถ้ามันยิ่งมีจำนวนมากขึ้นก็จะยิ่งทำให้ชุดทดสอบเราทำงานช้าขึ้นเรื่อยๆ ลองมาดูตัวอย่างของเทสกันครับ
describe('Pricing plan page', () => {
context('Product Plan Section', () => {
it('Should see Product title', () => {
cy.signUp()
cy.visit('/plan')
cy.get('.plan-title').should('contain', 'Basic Plan')
})
it('Should see price', () => {
cy.signUp()
cy.visit('/plan')
cy.get('.plan-price').should('contain', '$599')
})
})
})
จากโค้ดตัวอย่างข้างต้น จะเป็นการเทสว่าหน้า Pricing Plan เราเนี่ยมันมีชื่อ Plan กับราคาอยู่ ซึ่งจะเห็นว่าการจะทดสอบ TestCase นึงนี่ เราต้องเสียเวลาไปรัน command cy.signUp() ซึ่งเป็น command ที่เราสร้างให้เอาไว้สมัครสมาชิกเข้า service เรา ซึ่งเวลาสมัครใช้งานเนี่ย มันจะมีการทำงานหลายอย่างเกิดขึ้นไม่ว่าจะเป็น สร้าง subscription user, ส่งอีเมล์ยืนยันการสมัคร, ฯลฯ แต่ละอย่างนี่ใช้เวลาทั้งนั้นครับ
เปลี่ยนวิธีคิด 🤔
จาก Test Suite ข้างบนนะครับ พอมี TestCase ใหม่เพิ่มขึ้นด้วยสัญชาติญาณ Copy-Paste ของ Developer สิ่งที่เราจะทำก็คือ Copy step ของเทสเคสเก่าออกมาแล้วแก้ในส่วนที่จะ Assertion ซึ่งก็ยิ่งเป็นการเพิ่มเวลาที่ใช้รัน cy.signUp() ให้เพิ่มขึ้นไปอีกตัว จนเขียนเทสแก้ทีนึงก็ไปกดน้ำ ชงกาแฟรอได้เลยกว่าจะรันเสร็จ
พอเอาเรื่องนี้ไปปรึกษาเทพบอส Automate Tester ประจำทีมเราที่มีประสบการณ์การทำ Automate test มาอย่างโชคโชน เทสบอสก็ให้คำแนะนำที่โคตรเทพกลับมาว่า
แทนที่เราจะ sign up ใหม่ทุกครั้ง ทำไมเราไม่ setup test data แล้ว login แทนหละ
ประโยคอาจจะไม่ตามนี้เป๊ะๆ แต่ไอเดียมันประมาณนี้แหละ พอได้ยินแบบนั้นหน้าตาเทสเราเปลี่ยนไปเลยครับ
describe('Pricing plan page', () => {
context('Product Plan Section', () => {
it('Should see Product title', () => {
cy.login()
cy.visit('/plan')
cy.get('.plan-title').should('contain', 'Basic Plan')
})
it('Should see price', () => {
cy.login()
cy.visit('/plan')
cy.get('.plan-price').should('contain', '$599')
})
})
})
เสียดายมันทำ highlight สีไม่ได้แต่จุดที่เปลี่ยนคือแทนที่เราจะทำ command cy.signUp() ซึ่งใช้เวลานานมากในการทำงานอย่างที่กล่าวไว้ข้างต้น เราก็ setup test data ในเครื่องเป้าหมายให้เรียบร้อยก่อน ในทีนี้คือ User และข้อมูลที่เราจะ assert ที่ครอบคลุมเกือบทุกเคส หลังจากนั้นเราเปลี่ยนไปใช้ command cy.login() ซึ่งเป็น command ที่ใช้เวลาในการทำงานน้อยกว่า sign up เยอะระดับนึงเลย (เพราะไม่ต้องกรอกข้อมูลอะไรนอกจาก user / pass แล้วกด login
Don’t Repeat Yourself 👣
พอ Test Suite มันออกมาหน้าตาแบบนี้แล้ว ด้วยสัญชาติญาณ Developer ซึ่งเกลียด Duplication เข้าไส้ เราเลยจะย้ายโค้ดซ้ำๆ มาไว้ที่เดียวกัน ซึ่ง Cypress มีจุดให้เรา setup data ก่อนจะรันเทสทุกครั้งครับ ตรงนั้นครับเรียกว่า beforeEach() ซึ่งพอย้ายแล้ว Test Suite เราก็จะออกมาหน้าตาประมาณนี้
describe('Pricing plan page', () => {
context('Product Plan Section', () => {
beforeEach(() => {
cy.login()
cy.visit('/plan')
})
it('Should see Product title', () => {
cy.get('.plan-title').should('contain', 'Basic Plan')
})
it('Should see price', () => {
cy.get('.plan-price').should('contain', '$599')
})
})
})
ซึ่งจะเห็นว่าแต่ละ TestCase มันก็ยังรัน cy.login() และ cy.visit() ให้ครับ แต่ตัว test suite หน้าตาเราจะสะอาดขึ้นมาเพราะโค้ดซ้ำๆ ถูกย้ายไปไว้ใน beforeEach แล้ว
ไปให้สุดแล้วหยุดที่ before 🔥
พอเราย้ายโค้ดที่ login มาไว้ใน beforeEach แล้วจะเห็นว่าถึงแม้มันจะลดโค้ดซ้ำๆ ได้ แต่มันก็ยังทำซ้ำๆ อยู่ๆ ดี หมายความว่า เราต้องเสียเวลา login ทุกครั้งที่เริ่มทุก TestCase เลยเกิดไอเดียว่า แล้วถ้ามัน login แค่ครั้งแรกครั้งเดียวละ มันต้องรันเร็วขึ้นแน่ๆ เลย
โชคดีที่นอกจาก Cypress จะมี beforeEach ให้ใช้แล้ว ยังมี before ให้ใช้ด้วยครับ ซึ่ง before จะต่างจาก beforeEach คือมันจะรันแค่ตอนเริ่มต้นของ context ครั้งเดียวเท่านั้น พอเรามีความรู้ตรงนี้ปั๊ป โค้ดมันเลยถูกปรับให้เป็นแบบนี้ครับ
describe('Pricing plan page', () => {
context('Product Plan Section', () => {
before(() => {
cy.login()
})
beforeEach(() => {
cy.visit('/plan')
})
it('Should see Product title', () => {
cy.get('.plan-title').should('contain', 'Basic Plan')
})
it('Should see price', () => {
cy.get('.plan-price').should('contain', '$599')
})
})
})
แต่ว่าสิ่งที่เกิดขึ้นไม่เป็นแบบที่เราคิดครับ พอเราเปลี่ยนมาถึงจุดนี้ TestCase พังกระจายเลยครับ และบอกว่ามันไม่ได้ login ซึ่งทำให้เราประหลาดใจมาก เพราะเอ๊ะมัน login แล้วนิ มันน่าจะทำงานได้นะ
สิ่งที่เกิดขึ้นก็คือ Cypress มันฉลาดมากครับ โดยก่อนจะเริ่มแต่ละ TestCase มันจะทำการ Clear browser state ให้ซึ่ง state ที่ว่านั้นก็คือ Cookie นั้นเองครับ เพราะว่าเวลาเรา Login web service เนี่ย มันต้องทำการเก็บ state ว่ามัน Login อยู่ไว้ซักที่ครับ ในเคสของเรา webservice เรา based on Django Web Framework ครับ โดยหลังจาก login ตัว state จะถูกเก็บไว้ที่ Cookie ที่ชื่อ sessionid และอีกอันนึงคือ csrftoken ครับ
พอเรารู้แบบนี้ สิ่งที่เราคิดต่อมาคือ เราจะทำยังไงให้ Cypress มันไม่ clear cookie ก่อนจะเริ่มรันแต่ละเทส หลังจากค้น Document มาเราก็ไปเจอ command ข้างล่างนี้ครับ
Cypress.Cookies.preserveOnce()
โดย command นี้จะมีหน้าที่บอกว่าก่อนเริ่มต้น TestCase ใหม่ ไม่ต้อง reset cookie 2 ตัวที่ระบุทิ้งไว้นะโว้ย เราเลยเอามาปรับใช้กับ Test Suite ของเราออกมาหน้าตาเป็นแบบนี้ครับ
describe('Pricing plan page', () => {
context('Product Plan Section', () => {
before(() => {
cy.login()
})
beforeEach(() => {
Cypress.Cookies.preserveOnce('sessionid', 'csrftoken')
cy.visit('/plan')
})
it('Should see Product title', () => {
cy.get('.plan-title').should('contain', 'Basic Plan')
})
it('Should see price', () => {
cy.get('.plan-price').should('contain', '$599')
})
})
})
ทีนี้เราก็สามารถทำให้ Test Suite เรา Login ครั้งเดียวแล้วรัน TestCase ทั้งหมดใน context ได้แล้วครับ
เอามันไปอยู่ร่วมกับ TestCase อื่น 👩👩👧👧
พอเรา Optimize Test Suite นี้จนไวขนาดนี้แล้วก็ได้เวลารัน Test Suite ทั้งหมดแล้ว ซึ่งพอเรารันครั้งแรกก็เจอปัญหาเลยครับ เพราะว่ามันไม่ได้เคลียร์ cookie ให้ ทำให้ Test Suite ที่เกี่ยวกับ Sign up จริงๆ พังหมดเลยเพราะ state มันไม่ได้เริ่มต้นที่ unauthenticate user
ความคิดแรกที่คิดคือ เราต้อง Clear Cookie ตอนรันจบ Test Suite นี้ ซึ่งสัญชาติญาณแรกคือมี before ก็ต้องมี after แต่พอเราเอาไปใส่ after เทพบอสคนเดิมก็ได้ให้คำแนะนำที่ดีมากมาอีกแล้วว่า
ถ้าเราเอา step clearCookie นี่ไปไว้ใน after ถ้า TestCase เราพังระหว่างทางแล้วใครจะเป็นคน clearCookie หล่ะ มันเลยควรจะไปอยู่ใน before ก่อนทุกๆ Step ใน TestSuite เรา จะเป็นการรับประกันว่ามันจะถูก clearCookie แน่นอนก่อนเริ่ม Test Suite นี้
จากความรู้ตรงนี้ Test Suite สุดท้ายของเราเลยออกมาหน้าตาแบบนี้ครับ
describe('Pricing plan page', () => {
context('Product Plan Section', () => {
before(() => {
cy.clearCookies()
cy.login()
})
beforeEach(() => {
Cypress.Cookies.preserveOnce('sessionid', 'csrftoken')
cy.visit('/plan')
})
it('Should see Product title', () => {
cy.get('.plan-title').should('contain', 'Basic Plan')
})
it('Should see price', () => {
cy.get('.plan-price').should('contain', '$599')
})
})
})
Recap 🛡
ก่อนจะจบเรามาทวนกันก่อนครับว่าใน blog นี้เรารู้อะไรบ้าง
- ใน E2E เราควรลด Operation ที่ใช้เวลานานให้น้อยที่สุดเราจะได้ Feedback อย่างรวดเร็ว
- Cypress มี beforeEach ให้ใช้กรุ๊ป command ที่เราต้องการรันก่อนทุก TestCase แต่ถ้าอยากรันครั้งเดียวใน Context จะมี command before ให้
- เรารู้จักว่า Cypress จะจัดการเกี่ยวกับ Cookie ให้เราเสมอ ถ้าอยากเก็บ Cookie ไว้ให้ใช้ Cypress.Cookies.preserveOnce แต่ถ้าอยากลบให้ใช้ cy.clearCookies()
ก็หวังว่า blog นี้จะมีประโยชน์นะครับสำหรับใครที่เริ่มรู้สึกว่า E2E รันช้าเหลือเกิน แล้วพบกันใหม่ครับ!
Original post at: https://yothinix.medium.com/ใครว่า-e2e-เทสรันช้า-เทคนิคง่ายๆ-ที่จะทำให้-cypress-รันเร็วติดจรวด-6ef6cc775ca0