Cypress คือเครื่องมือที่ช่วยให้เราสร้างชุดทดสอบ Frontend Web ได้อย่างง่ายดาย แถมยังมีฟีเจอร์ต่างๆ เช่น debugger, time travel, automatic waiting, screenshot & video และ cross browser testing ให้เราได้ใช้งานเสร็จสรรพโดยไม่ต้องไปหาติดตั้งจากที่ไหนเพิ่มเติม
บทความนี้สรุปเนื้อหาเบื้องต้นเกี่ยวกับการใช้งาน Cypress ให้สามารถกลับมาทบทวนได้ใหม่ หรือเพื่อให้เห็นภาพรวมของ Cypress เท่านั้น ไม่ได้มีตัวอย่างแบบ In Action ให้ทำตามได้
Getting Start
เราสามารถติดตั้ง Cypress ได้ง่ายๆ ผ่าน npm
npm install cypress --save-dev
หลังจากติดตั้งเสร็จแล้วให้รัน cypress ขึ้นมาด้วยคำสั่ง
npx cypress open
จะมีหน้าจอของ Cypress Runner แสดงขึ้นมา ให้เปิดทิ้งไว้ แล้วเราจะพบว่า Cypress ได้สร้างตัวอย่างของ test script ต่างๆ ไว้ให้เราไว้ใช้ศึกษาต่อเพิ่มเติม ซึ่ง test เหล่านี้เราสามารถ click แต่ละ test แล้วสั่งรันได้เลยทันที เราจะเห็นผลการรัน test ที่เป็นลักษณะคล้ายกับการจำลองการใช้งานเว็บโดย user เองจริงๆ (แต่มันจะเร็วๆ หน่อย)
ลองสร้าง test file ของเราเองขึ้นมา เก็บไว้ใน folder integration
ตั้งชื่อว่า hello.spec.js
แล้วเราจะเห็นชื่อไฟล์ของเราปรากฎขึ้นมาทันที ที่หน้าต่างของ Cypress ที่เราเปิดทิ้งไว้ Cypress ตอนแรก
เราจะลองเขียน test ง่ายๆ ลงในไฟล์นี้ โดยโจทย์ที่เราต้องการทดสอบก็คือ ให้เปิดเว็บ Kitchen Sink แล้วมองหา link type จากนั้น click ที่ link พอ click เสร็จแล้ว เว็บจะเปลี่ยนไปอีกหน้าหนึ่งตาม link ที่เรา click เราจะเช็คว่า url ได้เปลี่ยนไปที่หน้า type ตามที่เราต้องการจริงหรือไม่ โดยเช็คจาก url ของหน้าที่เราต้องการ
จากความต้องการดังกล่าวเราสามารถเขียนโค้ดสำหรับ test ได้ดังนี้
it('Visit the Kitchen Sink and Click link', () => {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
cy.url().should('contains', 'commands/actions')
})
เราจะเขียน test แต่ละอัน โดยใช้ฟังก์ชั่น it()
ข้างในนั้นบรรจุขั้นตอนต่างๆ ที่เราต้องการทดสอบเอาไว้
จากโค้ด จะเห็นว่าขั้นตอนค่อนข้างตรงไปตรงมา เช่นเดียวกับโจทย์ที่เราได้ตั้งไว้ตั้งแต่ตอนแรกเลย
ให้ปิด Cypress ไปก่อน แล้วมาลองรัน test ของเราผ่าน command line โดยใช้คำสั่ง
npx cypress run —spec "cypress/integration/hello.spec.js"
เราจะเห็นผลลัพธ์ของการรันผ่านทาง command line ได้เลย จากนั้นให้ลองแก้ test ให้ไม่ผ่าน แล้วรันคำสั่งเดิมอีกครั้ง เราจะพบว่ามี screenshot และ video อยู่ใน folder ชื่อ screenshots
และ video
ของ test ที่ไม่ผ่านถูกเพิ่มเข้ามา แปลว่าต่อจากนี้ไปหากมี test ไหนที่ไม่ผ่าน จะมีวีดีโอและ screenshot เป็นหลักฐานเอาไว้ให้เราตามดูได้ในภายหลัง
Core Concept
หลังจากที่ลองสัมผัสกับ Cypress แบบไม่ต้องสนใจอะไรมาก ก็รันได้แล้ว เขียน test ง่ายๆ ได้แล้ว ต่อไปนี้คือสิ่งที่จำเป็นต้องเข้าใจเอาไว้เพื่อเป็นความรู้เบื้องต้น นำไปศึกษาต่อส่วนอื่นๆ ของ Cypress ด้วยตัวเองได้
การ test โดยทั่วไป จะใช้ 3 ขั้นตอนหลักต่อไปนี้
- Query DOM ที่เราต้องการ test จากบนหน้าจอ
- ส่ง command ไปยัง subject ที่เราสนใจ เช่นส่งให้ DOM element ที่เราเจอในข้อ 1
- Assertion
1. Query DOM element
เมื่อเราต้องการทดสอบอะไร เราจะต้องค้นหาสิ่งนั้นบนหน้าจอให้เจอก่อน ซึ่งก็คือเราจะต้องทำการ Query DOM
Cypress จะใช้ท่าในการ query DOM คล้ายกับ jQuery ดังนั้นคนที่ใช้ jQuery มาก่อนก็จะสามารถเข้าใจได้ไม่ยาก ใน Cypress นั้นก็เตรียมคำสั่ง (command) สำหรับการ query DOM element มาให้แล้ว เช่น get()
และ contains()
เป็นต้น
เรามักใช้ get()
ในการอ้างถึง DOM จากแท็ก หรือ attribute เช่นเดียวกับ jQuery เช่น
cy.get('#query-btn')
cy.get('.query-btn')
// Use CSS selectors just like jQuery
cy.get('#querying .well>button:first')
และเรามักใช้ contains()
ในการอ้างถึง DOM เมื่อเราต้องการอ้างถึงในมุมมองเสมือนกับที่ User กำลังใช้เว็บของเราจริงๆ เพราะเราจะอิงจากเนื้อหาที่ element นั้นๆ contain เป็นหลัก เช่น
cy.get('.query-list').contains('bananas')
// we can pass a regexp to `.contains()`
cy.get('.query-list').contains(/^b\w+/)
cy.get('.query-list').contains('apples')
// passing a selector to contains will
// yield the selector containing the text
cy.get('#querying').contains('ul', 'oranges')
cy.get('.query-button').contains('Save Form')
นอกจาก get()
และ contains()
แล้วยังมี command อื่นๆ ให้ใช้อีก สามารถอ่านเพิ่มเติมได้ที่ Examples of querying for DOM elements in Cypress
นอกจากนี้เมื่อเราได้ DOM element ที่ต้องการมาแล้ว เราสามารถลัดเลาะไปยัง element อื่นๆ ข้างเคียงได้อีกด้วย โดยใช้ command ในกลุ่ม Travesal ที่ Cypress เตรียมไว้ให้ เช่น children()
, filter()
, first()
, last()
เป็นต้น
cy.get('.traversal-breadcrumb').children('.active')
cy.get('.traversal-nav>li').filter('.active')
cy.get('.traversal-table td').first()
cy.get('.traversal-buttons .btn').last()
เราสามารถอ่านเพิ่มเติมคำสั่งสำหรับใช้ Traversal อื่นๆ ได้ที่ Examples of traversing DOM elements in Cypress
สิ่งที่ต้องเข้าใจไว้อย่างหนึ่งก็คือเมื่อเราใช้ command เพื่อ query DOM แล้ว Cypress จะไม่ return element กลับออกมาแบบ synchronouse เช่นเดียวกับ jQuery แต่ Cypress จะพยายาม Retry ไปเรื่อยๆ จนกว่าจะเจอ element ที่มี state ต้องการแล้วจึงทำ command ถัดไปต่อ หรือจนกว่าจะ timeout แล้วบอกว่า test นั้นๆ fail เป็นต้น
2. จัดการกับ Subject และการ Chain Command
การใช้ Cypress ก็คือการส่ง command ต่างๆ ที่ Cypress ได้เตรียมไว้ให้ ไปยัง cy
ที่เห็นได้จากโค้ดตัวอย่างต่างๆ ที่ยกมาก่อนหน้านี้
command ต่างๆ ดังกล่าว ถูกจัดกลุ่มไว้ให้เราสามารถเข้าไปศึกษาต่อได้โดยง่ายที่เว็บตัวอย่างของ Cypress เองที่ Kitchen Sink เว็บนี้ดีมาก มีทุก command และมีตัวอย่าง พร้อมกับ DOM element เตรียมไว้ให้เราลองรัน command ต่างๆ ดูได้เลยอีกด้วย
แต่ละ command ของ Cypress นั้นบางทีจะ yield บางอย่างออกมา เราเรียกสิ่งนั้นว่า Subject ซึ่งนั่นเป็น output ของการรัน command นั้นๆ นอกจากนี้ Cypress ยังสามารถส่งต่อ output นั้นไปยัง command อื่นเป็นทอดๆ ต่อไปกันเรื่อยๆ ได้อีกด้วย การทำแบบนี้เราเรียกว่าการ chain command
เหตุผลที่ใช้คำว่า yield ก็เพราะว่า command ของ Cypress จะไม่ทำงานแบบ synchronous และไม่ return อะไรกลับออกมาให้เราใช้ แต่จะจัดการการและทำงานคล้ายกับ Promise ใน JavaScript แทนเรา
เราสามารถอ้างถึงสิ่งที่แต่ละ command yield ได้โดยการใช้ then()
เช่น
cy
.get('#some-link')
.then(($myElement) => {
const href = $myElement.prop('href')
return href.replace(/(#.*)/, '')
})
เมื่อเราส่ง command ไปยัง Cypress และเรากดรัน test Cypress จะนำแต่ละคำสั่งใส่ไว้ใน queue เมื่อ Cypress อ่านจนจบไฟล์แล้ว ถึงจะ deque เอาแต่ละคำสั่งออกมาทำงานในภายหลังทีละคำสั่งจนจบ ดังนั้นการเขียนโค้ดแบบนี้อาจไม่ได้ทำงานตามที่ตั้งใจไว้
it('does not work as we expect', () => {
cy.visit('/my/resource/path') // Nothing happens yet
cy.get('.awesome-selector') // Still nothing happening
.click() // Nope, nothing
let el = Cypress.$('.new-el') // evaluates immediately as []
if (el.length) { // evaluates immediately as 0
cy.get('.another-selector')
} else {
cy.get('.optional-selector')
}
})
จากโค้ดที่ยกมา คำสั่งตั้งแต่บรรทัดที่ 7 เป็นต้นไป จะทำงานก่อนคำสั่งบรรทัดที่ 2-5 เพราะ Cypress จะนำ command ที่เป็น asynchronous เก็บลงใน queue ก่อนแล้วค่อยเอาออกมาทำงานหลังจากอ่าน test นี้จบแล้ว ส่วนคำสั่งตั้งแต่บรรทัดที่ 7 เป็นต้นไปคำสั่งที่ทำงานแบบ synchronous นั่นแปลว่ามันจะทำงานทันทีเรียงลำดับกันไปตั้งแต่ตอนอ่านไฟล์
ดังนั้นหากต้องการให้คำสั่งบรรทัดที่ 7 เป็นต้นไปทำงานเรียงลำดับ จะต้องใช้ then()
เข้าช่วย
3. Assertion
Cypress เตรียม command สำหรับการทำ assertion ไว้ให้เราใช้ แบ่งเป็น 2 กลุ่ม เป็นการใช้งานแบบ implicit และ explicit หรือพูดง่ายๆ ก็คือเป็นการใช้งานระหว่างกลุ่มแรกเป็น should()
, and()
กับอีกกลุ่มคือ expect()
, assert()
เราจะใช้ should()
ในการตรวจสอบสถานะของ subject ที่เราได้มา ส่วนการใช้ and()
นั้น ก็จะใช้เช่นเดียวกับ should()
ต่างกันแค่ชื่อที่ไม่เหมือนกันเพื่อให้อ่านได้ง่ายขึ้นเท่านั้น
ส่วนการใช้ assert()
กับ expect()
เราก็จะนำมาใช้ใน should()
, and()
อีกที
ตัวอย่างการใช้งาน
cy.get('.error').should('be.empty')
cy.contains('Login').should('be.visible')
cy.get('nav').should('be.visible')
cy.get(':checkbox').should('be.disabled')
cy.get('form').should('have.class', 'form-horizontal')
cy.get('input').should('not.have.value', 'Jane')
cy.get('button').should('have.id', 'new-user')
cy.get('#header a').should('have.attr', 'href', '/users')
cy.get('#input-receives-focus').should('have.focus')
หรือถ้าเราจะอ้างถึง subject นั้นตรงๆ เพื่อตรวจสอบอะไรบางอย่างเองเลยก็สามารถกำหนด function ให้กับ should()
ได้เลยตรงๆ แบบนี้
cy.get('.docs-header')
.find('div')
.should(($div) => {
expect($div).to.have.length(1)
const className = $div[0].className
expect(className).to.match(/heading-/)
})
ข้อควรระวังก็คือ ฟังก์ชั่นที่เรากำหนดให้กับ should()
นั้น จะต้องสามารถทำงานซ้ำไปซ้ำมาได้เรื่อยๆ โดยไม่กระทบกับโค้ดส่วนอื่น (retry-safe) เนื่องจาก Cypress จะ retry การทำงานของ should()
ไปเรื่อยๆ จนกว่าจะสำเร็จหรือ timeout ไป และอีกข้อหนึ่งก็คือการ return
ภายใน should()
นั้นไม่มีผลใดๆ กับ subject ที่ถูก yield
ออกมา
Hook
Cypress มี hook function ต่างๆ ให้เราใช้งาน เรียงตามลำดับดังนี้
beforeEach(() => {
// root-level hook
// runs before every test
})
describe('Hooks', () => {
before(() => {
// runs once before all tests in the block
})
beforeEach(() => {
// runs before each test in the block
})
afterEach(() => {
// runs after each test in the block
})
after(() => {
// runs once after all tests in the block
})
})
ใน Best Practice บอกไว้ว่า เราไม่ควรใช้ after()
และ afterEach()
ในการ clean up state แต่ให้ไป clean up state ที่ before
แทน
Debugger
เราสามารถ debug โค้ดของเราได้โดยใช้คำสั่ง debugger
หรือ command debug()
ที่ Cypress เตรียมไว้ให้เราได้
it('let me debug like a fiend', () => {
cy.visit('/my/page/path')
cy.get('.selector-in-question')
debugger // Doesn't work
})
จากโค้ดที่ยกมา เนื่องจาก Cypress จะนำ command ต่างๆ ลงไปใน queue ก่อนแล้วถึงจะ deque ออกมารันทีละคำสั่งตอนท้าย แต่นั่นไม่รวมถึง debugger
ซึ่งนั่นก็เท่ากับว่าการที่โค้ดวิ่งมาหยุดที่ debugger
นั่นเปล่าประโยชน์ เพราะเราจะไม่เห็นอะไรเลย เนื่องจาก command ต่างๆ ก่อนหน้านั้นยังไม่ทำงาน
เราจึงเปลี่ยนมาใช้ debugger
ใน then()
เพื่อหยุดดูค่าต่างๆ ที่กำลังเกิดขึ้นแทนดังนี้
it('let me debug when the after the command executes', () => {
cy.visit('/my/page/path')
cy.get('.selector-in-question')
.then(($selectedElement) => {
// Debugger is hit after the cy.visit
// and cy.get command have completed
debugger
})
})
กับการหยุดการทำงานอีกแบบที่ดูสะดวกกว่านั้น ก็คือการเพิ่ม debug()
ลงไปใน command chain เลย ดังนี้
it('let me debug like a fiend', () => {
cy.visit('/my/page/path')
cy.get('.selector-in-question').debug()
})
เมื่อ Cypress Runner หยุดทำงานตรงจุดที่เราใส่คำสั่ง debug ไว้ เราก็สามารถเปิด Debugger Console ของ Web Browser ขึ้นมาดูได้ตามปกติ มีประโยชน์มากตอนที่เราใช้ query command แล้วไม่แน่ใจว่า query มาถูกหรือไม่ หรือ query แล้วมี attribute และค่าต่างๆ อะไรให้เราเอามาใช้งานต่อได้
Alias
เราสามารถใช้ as()
ในการอ้างถึง subject ต่างๆ ได้อีกรอบหลังจากที่เรา yield ได้มาแล้ว ทำให้เราไม่จำเป็นต้องเขียนโค้ดเพื่อ yield subject นั้นๆ ซ้ำอีกครั้ง ซึ่งบางครั้งมันก็ยาวมากและซ้ำซ้อน และที่สำคัญก็คือ ทำให้เราแก้โค้ดที่ yield subject ได้จากที่เดียวได้อีกด้วย ไม่ต้องไปตามแก้ทุกที่ เช่น
cy.get('ul#todos').as('todos')
cy.get('@todos')
cy.get('table').find('tr').as('rows')
cy.get('@rows').first().click()
หรือใช้แชร์ context กันระหว่างรัน test ก็ได้ เช่น ก่อนแต่ละ test ให้ไป query หา submit button ก่อน เพื่อจะนำ submit button นั้นไปใช้ใน test อื่นๆ ต่อไปภายหลัง ก็ทำได้ดังนี้
beforeEach(() => {
cy.get('button[type=submit]').as('submitBtn')
})
it('disables on click', () => {
cy.get('@submitBtn').should('be.disabled')
})
Screenshot & Video
เมื่อเรารัน Cypress ด้วยคำสั่ง
cypress run
เมื่อมี test ที่ fail เกิดขึ้น Cypress จะ capture screenshot และ video ไว้ใน folder cypress/screenshots
และ cypress/videos
ให้โดยอัตโนมัติ แต่จะไม่ทำให้เมื่อเรารัน test ผ่านคำสั่ง cypress open
นอกจากนี้เรายังสามารถ manual ทำ screenshot ได้ด้วยคำสั่ง
cy.screenshot()
และทุกครั้งที่เราจะรันคำสั่ง cypress run
ใหม่ Cypress จะเคลียร์ screenshot และ video ทิ้งไปก่อน ซึ่งหากเราไม่ต้องการให้เคลียร์ทิ้ง เราสามารถตั้งค่าไว้ในไฟล์ cypress.json
ได้ โดยกำหนดค่า trashAssetsBeforeRuns
ให้เป็น false
ดูเรื่อง configuration เพิ่มเติม และการใช้งาน Screenshot & Video
Seeding Data
เราสามารถใช้ command cy.exec()
และ cy.request()
ในการ seed data ให้กับระบบเราก่อน test ได้ สามารถนำไปใส่ใน hook ต่างๆ ตามต้องการ เช่น beforeEach
เป็นต้น เช่น
describe('The Home Page', () => {
beforeEach(() => {
// reset and seed the database prior to every test
cy.exec('npm run db:reset && npm run db:seed')
// seed a user in the DB that we can control from our tests
cy
.request('POST', '/test/seed/user', { name: 'Jane' })
.its('body')
.as('currentUser')
})
it('successfully loads', () => {
cy.visit('/')
})
})
Cypress Dashboard
Cypress มีบริการ dashboard ให้เรา โดยเราแค่ไปสร้าง account ไว้ที่ https://dashboard.cypress.io หลังจากนั้นก็เข้าไปสร้าง project
เมื่อเราสร้าง project เราจะได้ projectId
และ key
มา ให้เรานำ projectId
มาใส่ไว้ใน cypress.json
ที่อยู่ที่ root ของโปรเจ็ค (ระดับเดียวกับ folder cypress)
{
"projectId": "1234"
}
และนำ key มาใช้ตอนเราสั่ง run
ดังนี้
npx cypress run --record --key 12345678987654321
หลังจากเราสั่ง run โดยการกำหนด key
และ projectId
แล้วเข้ามาดูที่เว็บ dashboard ก็จะเห็นผลลัพธ์การรัน และ video / screenshot ต่างๆ ก็จะถูกอัพโหลดขึ้นมาเก็บไว้ให้เราดูออนไลน์จากที่นี่หมดเลยเช่นกัน
Reference อื่นๆ ที่น่าสนใจ ที่ควรอ่านให้เข้าใจก่อนนำ Cypress ไปใช้งาน