เริ่มต้นเขียน Automated Test ด้วย Cypress.io แบบลงมือทำ

J A N E z~*
Nellika
Published in
5 min readNov 6, 2018

สวัสดีค่ะ ~ หลังจากที่ช่วงนี้พยายามลองหาอะไรใหม่ ๆ เล่น บวกกับต้องทำ Automated Test ในโปรเจคที่ทำอยู่แล้ว ซึ่งก่อนหน้านี้ตัวเองเคยได้เขียน Selenium Framework มาอยู่แล้วบ้าง แต่รู้สึกยังไม่ตอบโจทย์เท่าไร ด้วยปัญหาที่เคยเจอ เช่น

  • ตัว Test ตัวเดียวกัน แต่ถ้าไปรันบนคอมพิวเตอร์ที่ Performance ต่างกัน บางครั้งจำเป็นต้องเปลี่ยนตัวเลขของคำสั่ง Wait เช่น จาก รอเวลา 3 วินาที อีกเครื่องอาจต้องใช้เวลา 5 วินาที ในการรอ
  • ตัว Test ที่รันแล้วผ่านเมื่อวาน อาจไม่ผ่านสำหรับวันนี้ (เมื่อวานยังใช้ได้อยู่เลยนะ)
  • ต้องคอยอัพเดต Web Driver อยู่บ่อย ๆ เนื่องจากบราวเซอร์ที่ใช้มีการเปลี่ยนแปลง ตัว Web Driver ที่ใช้ลงในตัว Selenium อาจทำงานได้ไม่สมบูรณ์นัก

ทันใดนั้นเอง … สวรรค์ก็เหมือนจะเมตตาเดฟตัวน้อย ๆ ท่ีพยายามหาอะไรมาทดแทนเจ้า Selenium อยู่หลายวัน ก็ไปเจอบทความข้างล่าง ผ่านการแชร์บนเฟซบุ๊คเพื่อน (ขอบคุณพี่ Traitanit สำหรับบทความเจ๋งๆมา ณ ที่นี้ -/\-)

อ่านจนจบบทความ ก็พบว่า เฮ้ย มันน่าจะเป็นตัวเลือกที่ดี และก็ไม่ใช่เราคนเดียวที่เจอปัญหาของเจ้า Selenium ซะหน่อย โดยเฉพาะตัวเทสที่รันผ่านบ้างไม่ผ่านบ้าง แต่ … 10 บทความอ่าน ก็ไม่เท่า 1 ครั้งที่ลงมือทำ เพราะฉะนั้นบทความนี้เราจะเริ่มต้นเรียนรู้ไปพร้อม ๆ กันค่ะ < 3

รู้จัก Cypress.io

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 TestingCypress ตั้งใจให้การเขียนเทสออกมาดีที่สุด ดังนั้นจึงอยากให้มีการเขียนได้ครบทั้งระบบ ตัวเดียวจบ ตามที่กล่าวไปข้างต้น อารมณ์ประมาณว่า พี่ไม่ได้มาเล่น ๆ
  • จะเฟรมเวิร์กไหนก็มาเถอะ — หมดปัญหาเรื่องความต่างของเฟรมเวิร์ก จะ 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

เพื่อไม่ให้บทความมันยืดเยื้อเกินไป เริ่มลงมือติดตั้งกันเลยดีกว่า

  1. สำรวจก่อนว่ามี 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:open

Cypress ได้เขียน Spec File เพื่อให้เราได้ลองเข้าไปเล่นคลิกเล่นดู ขณะนี้ ยังซัพพอร์ตบราวเซอร์ได้แค่ Chrome นะคะ

จากนั้นจะพบว่ามันเล่นเร็วซะจนมองไม่ทัน (เวอร์ไป) โดยรันเทสเคสทั้งหมด 19 เทสเคส ใช้เวลาไป 63.53 วินาที !! … อู้วว เริ่มอยากลองเขียนแล้ว

โครงสร้างโฟลเดอร์

หลังจากที่เราลองรันคำสั่ง npm run cypress:open เราจะพบว่ามีโฟลเดอร์ cypress ปรากฎขึ้นมายังโปรเจคของเรา

โครงสร้างโฟลเดอร์ 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)

  1. เริ่มต้นสร้างไฟล์ในโฟลเดอร์ integration ชื่อ check_time_movie.spec.ts
  2. สิ่งที่ดีงาม คือ 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 ซ้ำ เราจึงต้องระบุเจาะจงคลาส โดยในส่วนของการเปลี่ยนภาษา เราจะเจอ
DOM Elements เปลี่ยนภาษา

สังเกตได้ว่ามีแค่ <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.io

ทิ้งท้าย

เสร็จสิ้นเรียบร้อยสำหรับการลองเล่นตัว Cypress โดยรวมเรียกได้ว่าค่อนข้างประทับใจค่ะ เพราะมีอะไรให้เล่นเยอะดี โดยเฉพาะการรันเทสที่ลื่นไหล ไม่ต้อง Wait อะไรให้เสียเวลา อีกทั้งยังสามารถ cy.get() Selector ต่าง ๆ อย่างต่อเนื่องได้ (มันจะอ้าง Selector ที่เรา get ไว้ล่าสุด) ทำให้สะดวกต่อการเขียน
แต่ถ้าใครไม่คุ้นชินกับ JavaScript อาจต้องใช้เวลาในการเรียนรู้สักนิด
ทั้งนี้หากมีอะไรผิดพลาด สามารถบอกได้เลยนะคะ ขอบคุณที่อ่านกันมาถึงตรงนี้ เจอกันบทความหน้า บับบาย ~

ดูโค้ดเต็มๆ ได้ที่นี่

--

--