เริ่มต้นเขียน Automated Test ด้วย Cypress.io แบบลงมือทำ
สวัสดีค่ะ ~ หลังจากที่ช่วงนี้พยายามลองหาอะไรใหม่ ๆ เล่น บวกกับต้องทำ Automated Test ในโปรเจคที่ทำอยู่แล้ว ซึ่งก่อนหน้านี้ตัวเองเคยได้เขียน Selenium Framework มาอยู่แล้วบ้าง แต่รู้สึกยังไม่ตอบโจทย์เท่าไร ด้วยปัญหาที่เคยเจอ เช่น
- ตัว Test ตัวเดียวกัน แต่ถ้าไปรันบนคอมพิวเตอร์ที่ Performance ต่างกัน บางครั้งจำเป็นต้องเปลี่ยนตัวเลขของคำสั่ง Wait เช่น จาก รอเวลา 3 วินาที อีกเครื่องอาจต้องใช้เวลา 5 วินาที ในการรอ
- ตัว Test ที่รันแล้วผ่านเมื่อวาน อาจไม่ผ่านสำหรับวันนี้ (เมื่อวานยังใช้ได้อยู่เลยนะ)
- ต้องคอยอัพเดต Web Driver อยู่บ่อย ๆ เนื่องจากบราวเซอร์ที่ใช้มีการเปลี่ยนแปลง ตัว Web Driver ที่ใช้ลงในตัว Selenium อาจทำงานได้ไม่สมบูรณ์นัก
ทันใดนั้นเอง … สวรรค์ก็เหมือนจะเมตตาเดฟตัวน้อย ๆ ท่ีพยายามหาอะไรมาทดแทนเจ้า Selenium อยู่หลายวัน ก็ไปเจอบทความข้างล่าง ผ่านการแชร์บนเฟซบุ๊คเพื่อน (ขอบคุณพี่ Traitanit สำหรับบทความเจ๋งๆมา ณ ที่นี้ -/\-)
อ่านจนจบบทความ ก็พบว่า เฮ้ย มันน่าจะเป็นตัวเลือกที่ดี และก็ไม่ใช่เราคนเดียวที่เจอปัญหาของเจ้า Selenium ซะหน่อย โดยเฉพาะตัวเทสที่รันผ่านบ้างไม่ผ่านบ้าง แต่ … 10 บทความอ่าน ก็ไม่เท่า 1 ครั้งที่ลงมือทำ เพราะฉะนั้นบทความนี้เราจะเริ่มต้นเรียนรู้ไปพร้อม ๆ กันค่ะ < 3
รู้จัก Cypress.io
Cypress ให้นิยามตัวเองว่าเป็น JavaScript End to End Testing พูดง่าย ๆ คือ มันสามารถเทสได้ครบทั้งระบบ ไล่ตั้งแต่ Unit Tests ไปจนถึง End-to-end Tests โดยใช้ภาษาจาวาสคริปต์ (หรือไทป์สคริปต์ก็ได้นะ) จากเดิมที่ใช้ไพธอนในการเขียน Selenium ตอนนี้ถ้าเขียน Cypress ก็ต้องเปลี่ยนภาษาแล้วนะ โดย Cypress จะมาขิงให้ชาวโลกได้รู้ว่า …
- ไม่ได้ใช้ Selenium — หลายคนอาจเข้าใจผิดว่า Cypress ทำงานโดยครอบ Selenium อีกที คำตอบคือ ไม่ใช่ ทาง Cypress ให้เหตุผลว่า เนื่องจาก Testing tool ในตลาดตอนนี้ทำงานโดยใช้ Selenium จึงทำให้เกิดปัญหาเดียวกัน เหมือน ๆ กัน ดังนั้นเพื่อแก้ไขปัญหานี้ จึงต้องออกแบบใหม่ เขียนใหม่หมดนั่นเอง
- End-to-end Testing— Cypress ตั้งใจให้การเขียนเทสออกมาดีที่สุด ดังนั้นจึงอยากให้มีการเขียนได้ครบทั้งระบบ ตัวเดียวจบ ตามที่กล่าวไปข้างต้น อารมณ์ประมาณว่า พี่ไม่ได้มาเล่น ๆ
- จะเฟรมเวิร์กไหนก็มาเถอะ — หมดปัญหาเรื่องความต่างของเฟรมเวิร์ก จะ React, Angular, Vue ก็ขอให้มา หรือจะเว็บเก่า ๆ ก็รองรับหมดอ่ะ (เจ๋งป่ะ)
- มัดรวมมาให้หมดแล้ว — การทำ Ent-to-end Testing อาจต้องใช้ไลบรารี่หลายตัวในการทำงานร่วมกัน แต่ Cypress เอามันมาไว้ในที่เดียว เป็น Bundled Tools เช่น Mocha, Chai, Sinon (ตามภาพด้านบน)
- คุณค่าที่ Developer & QA คู่ควร — เพื่อให้การทำงานเป็นประสิทธิภาพและทิศทางเดียวกันมากขึ้น จากเดิมที่ Dev ก็ต้องเขียน Unit Tests อยู่แล้ว QA ก็ต้องทำ Automated Tests ก็เขียนไว้ใน Cypress ที่เดียวสิ ภาษาเดียวจบ
- เร็วและไม่ Flaky — รู้ได้เองว่าอันไหนต้องรอ หรือไม่รอ เพื่อแก้ปัญหาการเทสผ่านบ้างไม่ผ่านบ้าง Cypress ก็ฉลาดพอที่จะรู้เอง หรือเราจะบอกให้มันรอก็ย่อมได้ อีกทั้งสิ่งที่นางบอกมาตลอดคือ มันเร็วมาก ๆๆ (โม้อ่ะเปล่า)
Getting Started
เพื่อไม่ให้บทความมันยืดเยื้อเกินไป เริ่มลงมือติดตั้งกันเลยดีกว่า
- สำรวจก่อนว่ามี NodeJS เพื่อใช้ในการติดตั้ง Cypress หรือไม่ ด้วยการเช็คเลขเวอร์ชัน ถ้ายังไม่มี ก็ไปติดตั้งก่อนนะจ๊ะ
node -v
2. สร้างโฟลเดอร์โปรเจคของเรา แล้ว cd เข้าพาธไป
mkdir test-cypress
cd test-cypress
3. เนื่องจากตอนนี้ยังเป็นโฟลเดอร์เปล่า ๆ ไม่มีอะไรเลย เราจึงจำเป็นต้องใช้คำสั่งด้านล่าง เพื่อให้มีตัว package.json
เกิดขึ้นมา
npm init
ในขั้นตอนนี้มันจะถามรายละเอียดเล็ก ๆ น้อย ๆ ของโปรเจคเรา แต่ใครขี้เกียจจะ Enter รัว ๆ ก็ไม่ว่ากัน
4. มาถึงพระเอกของบทความนี้กันแล้นนนน ติดตั้งไปเลย ลุย ๆๆ
npm install cypress --save-dev
5. จากนั้นเราจะทำการรันเจ้าตัว Cypress เพื่อให้ง่ายต่อการเรียกใช้ครั้งถัดไป เพิ่มคำสั่งนี้ลงใน package.json
ของเราค่ะ
{
"scripts": {
"cypress:open": "cypress open"
}
}
แล้วเริ่มต้นรันโปรเจคของเรา โดยใช้คำสั่ง
npm run cypress:open
แท่นแท๊นนน …. ~
Cypress ได้เขียน Spec File เพื่อให้เราได้ลองเข้าไปเล่นคลิกเล่นดู ขณะนี้ ยังซัพพอร์ตบราวเซอร์ได้แค่ Chrome นะคะ
จากนั้นจะพบว่ามันเล่นเร็วซะจนมองไม่ทัน (เวอร์ไป) โดยรันเทสเคสทั้งหมด 19 เทสเคส ใช้เวลาไป 63.53 วินาที !! … อู้วว เริ่มอยากลองเขียนแล้ว
โครงสร้างโฟลเดอร์
หลังจากที่เราลองรันคำสั่ง npm run cypress:open
เราจะพบว่ามีโฟลเดอร์ cypress ปรากฎขึ้นมายังโปรเจคของเรา
โดยแต่ละโฟลเดอร์มีไว้ทำอะไรบ้าง อธิบายคร่าว ๆ ได้ดังนี้
- fixtures ไว้เก็บข้อมูล Test Data เช่น ชื่อ, อีเมล
- integration เอาไว้เก็บพวก Test Case ต่าง ๆ โดย Test Case แต่ละไฟล์ที่เขียน จะต้องมี
name.spec.ts
เพื่อให้รู้ว่าเป็นเทสเคสนะ - plugins เก็บปลั๊กอินต่าง ๆ ที่จะมาช่วยเสริมกำลังของการเขียน Test Case (อันนี้ชอบมาก เพราะเป็นคนชอบติดตั้งปลั๊กอินนั่นนี่อยู่แล้ว ถถถ) หรือจะเขียนเองไปเลยก็ได้นะ
- support เก็บพวก Custom Command ที่เขียนขึ้นมาเอง หรือ Command ที่อยากจะ Overwrite มัน เพื่อเอาไว้ใช้ในตัวเทสเคสของเราอีกที
- screenshots โฟลเดอร์นี้จะถูกสร้างขึ้นมาก็ต่อเมื่อ เรารันคอมมานด์
cy.screenshot('name')
ที่ใช้สำหรับบันทึกหน้าจอ รูปภาพที่บันทึกสำเร็จแล้วก็จะมาอยู่โฟลเดอร์นี้
เริ่มเขียนเทสเคส
เริ่มต้นสร้างไฟล์มาสักไฟล์หนึ่ง ชื่อ sample_test.spec.ts
แล้วกัน (จะเห็นได้ว่าเมื่อสร้างไฟล์สำเร็จแล้ว ก็จะถูกแสดงในหน้าต่างของ Cypress เลย)
จัดการเขียนเทสเคสขึ้นมาง่าย ๆ สักหนึ่งอัน เช่น เราคาดหวังว่า 1+1 ต้องเท่ากับ 2 เขียนเทสเคสได้ ดังนี้
describe('My First Unit Test', function() {
it('Does not do much!', function() {
const sum = 1 + 1;
expect(2).to.equal(sum)
})
})
จะได้ผลลัพธ์ออกมาแบบนี้
หรือลองทำให้มัน Fail ดูไหม ก็ปรับโค้ดแบบนี้
describe('My First Unit Test', function() {
it('Does not do much!', function() {
const sum = 1 + 1;
expect(3).to.equal(sum)
})
})
ก็จะได้ผลลัพธ์แบบนี้
อธิบายกันสักหน่อย
describe()
คือ การบอกว่าเราจะเทสอะไร ก็ตั้งชื่อมันไปได้เลยit()
คือ การบอกว่าเราจะเทสอย่างไร โดยข้างในฟังก์ชันก็เขียน Test Step ลงไปexpect()
คือ ผลลัพธ์ที่เราคาดหวัง อยากให้เป็นอะไรequal()
คือ ผลลัพธ์ที่มันควรจะเป็น
เจ้า Cypress ชูโรงมาตลอดว่าตัวเองมีฟีเจอร์ Automatic Waiting นะ ทำให้เราไม่จำเป็นต้องเขียนว่า ส่วนไหนต้อง Sleep หรือ Wait อีกต่อไป เพราะฉะนั้นเดี๋ยวเราลองมาเขียนเทสเคสแบบอื่นกัน
> เช็ครอบหนัง
โจทย์: วันนี้ต้องการจะดูหนังเรื่องหนึ่ง เลยอยากรู้ว่าหนังเรื่องนี้ มีรอบฉายใกล้ ๆ เวลาที่เราสะดวกไหม (ใจจริงอยากเขียนให้มันซื้อตั๋วไปเลย แต่ช่วงนี้จน 555)
- เริ่มต้นสร้างไฟล์ในโฟลเดอร์ integration ชื่อ
check_time_movie.spec.ts
- สิ่งที่ดีงาม คือ Cypress มี
moment.js
ให้เราเรียกใช้ได้เลย ทำให้ไม่ต้องเสียเวลาติดตั้ง เราก็ประกาศตัวแปรได้แบบนี้
const url = 'https://www.sfcinemacity.com'
const todayDate = Cypress.moment().format('DD MMM YYYY')
const nowTime = Cypress.moment().format('HH:mm')
const expectTime = Cypress.moment().add(1, 'hours').format('HH:mm')
const nameMovie = 'Homestay'
const locationMovie = 'SFX CINEMA Central Rama 9'
3. จากนั้นเราจะเริ่มเปิดหน้าเว็บไซต์ขึ้นมา โดยใช้คำสั่ง cy.visit()
describe('Check Time Movie', () => {
it('Go to url', () => {
cy.visit(url)
})
})
4. เนื่องจากหน้าเว็บไซต์ SF Cinema มีค่าเริ่มต้นเป็นภาษาไทย จึงทำให้วันที่ของรอบฉายเป็นภาษาไทย เช่น 15 พ.ย. 2018 แม้ Cypress เองจะมี moment.js ให้ใช้ แต่ยังไม่สามารถเปลี่ยน locale ได้ ทำให้ต้องเปลี่ยนภาษาของหน้าเว็บ SF แทน
- ใช้คำสั่ง
cy.get()
สำหรับการเลือกภาษา เนื่องจากหน้าเว็บไซต์มีโอกาสที่จะมีคำว่า ENG ซ้ำ เราจึงต้องระบุเจาะจงคลาส โดยในส่วนของการเปลี่ยนภาษา เราจะเจอ
สังเกตได้ว่ามีแค่ <ul> เท่านั้นที่มีคลาส ส่วน <li> จะมีคลาสได้ก็ต่อเมื่อมันถูก active อยู่ เพื่อความชัวร์เราจึงต้องอ้างคลาสของ <ul> เราก็จะได้คำสั่งหน้าตาแบบนี้
cy.get('[class="lang-switcher"]>li')
แต่ว่าภายใต้ <ul> มีแท็ก <li> ตั้ง 2 ก้อนแหน่ะ แล้วก้อนไหนที่เราต้องการล่ะ จึงจำเป็นต้องวนลูปหาค่า ENG จากนั้นค่อยคลิกเปลี่ยนภาษานั่นเอง
it('Change language', () => {
cy.get('[class="lang-switcher"]>li').each($el => {
if ($el.get(0).innerText === 'ENG') {
cy.wrap($el).click()
}
})
// เพื่อความชัวร์เช็คอีกทีสิ ว่ามันเปลี่ยนภาษาจริงรึเปล่า
cy.get('[class="top-navigation"]').contains('Login/Sign up')
})
5. เมื่อได้รูปแบบหน้าเว็บไซต์ที่ต้องการแล้ว ก็ได้เวลาหารอบฉาย เริ่มจากการเลือกโรงภาพยนตร์ที่ต้องการ
- className ยังสามารถใช้ซ้ำกันได้ เช่นเคย เพื่อความแน่ใจแล้วว่าใช่ปุ่มที่เราต้องการจริง ๆ จึงเพิ่ม
contains()
เพื่อตรวจสอบว่าเป็นข้อความที่เราต้องการไหม แล้วค่อยคลิก
it('Select Cinema', () => {
cy.get('[class="button dropdown-button"]')
.contains('Select Cinema')
.click()
cy.contains(locationMovie)
.click()
})
6. เลือกภาพยนตร์ โก ๆๆ
it('Select Movie', () => {
cy.get('[class="button dropdown-button"]')
.contains('All Movie')
.click()
cy.get('h3[class="name"]')
.contains(nameMovie)
.click()
cy.get('[class="button showtime-button"]')
.contains('Showtime')
.click()
})
7. ยังไม่ชัวร์อ่ะ ว่าที่เลือกไปมันใช้ของวันนี้จริงรึเปล่า อ่ะ ๆๆ เช็คไว้ก่อน
it('Check Date Movie', () => {
cy.get('[class="selected"]>p')
.contains(todayDate)
})
8. ได้เวลาดูรอบฉายแล้ววววว เริ่มแรกเราก็ดึงรอบฉายทั้งหมดที่มีก่อน ความยากของมันคือ เราต้องการ className ที่ชื่อ time-list แต่มันอยู่ลึกมาก ๆ ทำให้การ get ค่าต้องดึงหลายรอบ ซึ่งเราจึงลักไก่ จากคำสั่งที่ Cypress มีให้ คือ children()
it('Check Time Movie', () => {
cy.get('[class="showtimelist"]>div')
.children()
.children()
.children()
.children()
})
เมื่อเราได้ className ที่ต้องการแล้ว ก็จะได้ค่าเวลาออกมา
it('Check Time Movie', () => {
cy.get('[class="time-list"]>li').each($list => {
console.log($list.get(0).innerText)
})
})
คอนโซลดูสักหน่อย
โดยสิ่งที่เราต้องการ คือ หารอบฉายที่ไม่เกิน 1 ชั่วโมง นับจากเวลาปัจจุบัน เขียนออกมาได้แบบเน้
it('Check Time Movie', () => {
cy.get('[class="time-list"]>li').each($list => {
if(
$list.get(0).innerText >= nowTime &&
$list.get(0).innerText <= expectTime
) {
cy.wrap($list.children()).click()
}
})
})
แต่ … ! ถ้ากรณีรอบหนังมันอยู่คาบเวลาเดียวกันมากกว่า 1 ล่ะ เช่น ขณะนี้เวลา 12:50 แต่ดันมีรอบหนัง 12:50, 13:15, 13:50 อยู่ในช่วงที่เรากำหนดไม่เกิน 1 ชั่วโมงพอดี จะทำไงดี แบบง่าย ๆ เราก็ใส่ return
มันซะ ให้คลิกแค่อันแรกที่เจอ แล้วไปทำเคสต่อไป
## อัปเดตเพิ่มเติม เราสามารถ stop each ด้วยการใส่ return false
ตาม doc cypress ได้เลยค่ะ
## อัปเดตเพิ่มเติม (อีกรอบ): จริง ๆ บทความนี้ยังไม่ครอบคลุมเงื่อนไข เป็นเพียงไกด์ไลน์เริ่มต้นเท่านั้น (อาจจะมีเงื่อนไขเพิ่มเติมอย่าง ถ้ารอบฉายไม่มี test ก็จะ failed ทันที) ดังนั้นจะต้องดูข้อมูลประกอบเพื่อเขียนเงื่อนไขให้ครอบคลุมด้วยนะคะ 😎
it('Check Time Movie', () => {
cy.get('[class="time-list"]>li').each($list => {
if(
$list.get(0).innerText >= nowTime &&
$list.get(0).innerText <= expectTime
) {
cy.wrap($list.children()).click()
return false
}
})
})
9. ตรวจสอบความเรียบร้อยสักหน่อย ว่าหน้าเว็บเปลี่ยนไป Step ถัดไปหรือยัง ก็เขียนง่าย ๆเลยเนาะ
it('Check Page Change', () => {
cy.contains('Selected Seat')
})
เรียบร้อยแล้วฮับ เวลาเฉลี่ยที่เทสอยู่ที่ 8–11 วินาที ขึ้นอยู่กับการรอ Loading ตอนเข้าหน้าเว็บ SF ครั้งแรก
ทิ้งท้าย
เสร็จสิ้นเรียบร้อยสำหรับการลองเล่นตัว Cypress โดยรวมเรียกได้ว่าค่อนข้างประทับใจค่ะ เพราะมีอะไรให้เล่นเยอะดี โดยเฉพาะการรันเทสที่ลื่นไหล ไม่ต้อง Wait อะไรให้เสียเวลา อีกทั้งยังสามารถ cy.get()
Selector ต่าง ๆ อย่างต่อเนื่องได้ (มันจะอ้าง Selector ที่เรา get ไว้ล่าสุด) ทำให้สะดวกต่อการเขียน
แต่ถ้าใครไม่คุ้นชินกับ JavaScript อาจต้องใช้เวลาในการเรียนรู้สักนิด
ทั้งนี้หากมีอะไรผิดพลาด สามารถบอกได้เลยนะคะ ขอบคุณที่อ่านกันมาถึงตรงนี้ เจอกันบทความหน้า บับบาย ~
ดูโค้ดเต็มๆ ได้ที่นี่