มาเขียน Automated UI Tests ให้กับ LIFF App ด้วย Cypress.io กันเถอะ

Traitanit Huangsri
LINE Developers Thailand
6 min readMar 1, 2020

สวัสดีครับ ผมเชื่อว่านักพัฒนาหลายๆ คนที่เคยเขียน LIFF App (LINE Frontend Framework) เพื่อนำไปใช้งานทั้งบนมือถือในแอพ LINE หรือบน external browser ทั่วไปคงจะเคยเจอปัญหาเดียวกับผมคือว่าการทดสอบแอพของตัวเองทำได้ค่อนข้างยากลำบาก จะเทสอะไรทีบางครั้งก็ต้องหยิบมือถือขึ้นมานั่งจิ้ม UI ในแอพของตัวเองที เวลาแก้โค้ดอะไรทีก็ต้องมารัน Manual Test กันจนเมื่อยมือ

จะดีกว่าไหม ถ้าเราสามารถสร้าง Automated Test ให้กับ LIFF App ของเราได้ โดยที่เราไม่ต้องมานั่งเทสด้วยตัวเอง และให้มันรันเทสทุกครั้งที่เรามีการแก้โค้ดเลย ก็จะทำให้ชีวิตเราดีขึ้นมากเลยจริงไหมครับ

ปัญหาของการทำ Automated Test บน LIFF App

ก่อนหน้านี้การที่เราต้องทำ Manual Test กับ LIFF App ของเราก็เป็นเพราะว่า LIFF SDK นั้นมี dependencies ที่ผูกติดกับ LINE Platform อยู่มากมาย ยกตัวอย่างเช่นเราต้องเรียก liff.init() เพื่อทำการ initialize LIFF App ของเราให้สามารถทำงานร่วมกับ LINE Platform ได้ หรือว่าจะเป็นการเรียก liff.login() เพื่อทำ authentication กับ LINE Login เป็นต้น

หลายคนที่อาจจะเคยลองเขียนเทส LIFF App ตัวเองอาจจะเคยเจอปัญหาเดียวกับผมลักษณะนี้ครับ

LIFF App and LINE Platform
  1. การทำ Integration Test กับ LINE Platform ทำให้เกิด Flaky Tests ได้ง่าย: ยกตัวอย่างเช่น การทำ LINE Login นั้นถ้าเป็นการรันแอพบน external browser ก็จะต้องมีการ redirect uri ไปยัง https://access.line.me เพื่อทำ OAuth Login กับ LINE Platform ซึ่งเป็น dependencies ที่ test ของเรา control ไม่ได้ ทำให้มีโอกาสเกิด Flaky Tests สูง คือเทสรันผ่านบ้าง ไม่ผ่านบ้าง โดยที่ไม่ได้แก้โค้ดใดๆ
  2. การจำลองการเทส LIFF App Testing บนแอพ LINE เอามาเทสบน Browser เป็นไปได้ยาก และเราก็ไม่ควรใช้ Mobile Testing Framework ในการทำเทส LIFF App ด้วยครับ เพราะ LIFF App = Web App ไม่ใช่ Mobile Native App ดังนั้นการเลือกใช้ Web Test Framework นั้นก็เหมาะสมกับการเทส LIFF App แล้วครับ แต่คำถามคือจะทำยังไงให้เรา Simulate Environment การรันแอพของเราให้เหมือนกับเวลาที่รันอยู่ใน LINE App นั่นแหละครับ
  3. ไม่สามารถ Simulate Edge Cases หรือ Negative Tests ได้: ยกตัวอย่างเช่น ถ้าสมมติเราต้องการจะเขียนเทสเพื่อ Simulate Error เวลาที่เรียก liff.init() และได้ Promise Reject ออกมาจากฝั่ง LINE นั้น แทบจะเป็นไปไม่ได้เลย เพราะส่วนใหญ่มันก็จะ init success เสมอถ้าเราเรียกใช้ LINE API ให้ทำงานได้ถูกต้องตามปกติ

แล้วเราจะเขียน Automated Test ให้ LIFF App เราได้ยังไงดี?

ก่อนอื่นต้องขอบอกก่อนเลยว่า เราจะไม่ใช้วิธีการดึง LIFF SDK ออกจาก Project ของ LIFF App ตอนที่รันเทสนะครับ เพราะนั่นจะทำให้เราไม่ได้เทส Behavior ของ App เราเมื่อทำงานร่วมกับ LIFF SDK จริงๆ ครับ

หลังจากที่ผมทำ Research มาซักพัก วันนี้ผมก็ได้คำตอบแล้วครับ ผมขอนำเสนอ Cypress.io ซึ่งมีความสามารถในการมาช่วยทำ Automated UI Test ของ LIFF App เราได้โดยที่เราไม่ต้องมานั่งทำ Manual Test อีกต่อไปครับ

Cypress คืออะไร?

Cypress คือ All-in-one Javascript Testing Framework สามารถใช้เขียนเทสได้ตั้งแต่ระดับ Unit, Integration ไปจนถึง End to End Test เลย ซึ่งจุดเด่นอย่างนึงของตัว Cypress คือมันจะรันอยู่บน run loop process เดียวกับ Web Application ของเรา ทำให้มันสามารถ access ทุกสิ่งอย่างที่อยู่ในแอพเราได้อย่าง native เลย ซึ่งล่าสุด Cypress รองรับการรัน Cross Browser Testing ทั้งบน Chrome, Edge (และ Chromium Based Browsers) และบน Mozilla Firefox แล้วด้วยนะครับ

นอกจากนี้ Cypress ยังมี features อื่นๆ ที่น่าสนใจอีกมากมาย ถ้าใครที่สนใจอยากทำความรู้จักเพิ่มเติม สามารถอ่านได้ที่บทความที่ผมเขียนไว้ด้านล่างนี้ครับ

แนวคิดและไอเดียการนำ Cypress มาใช้กับ LIFF

อย่างที่ผมได้บอกไปก่อนหน้านี้ว่า Cypress สามารถทำ native access ทุกอย่างที่อยู่ในแอพของเราได้ เพราะฉะนั้นไม่ว่าแอพเราจะมี Objects อะไรที่ใช้อยู่ เมื่อมันถูกรันด้วย Cypress แล้วเราสามารถทำให้ Objects เหล่านั้นถูก Control ด้วย Cypress ได้ครับ

ดังนั้นจากปัญหาที่ผมพูดถึงก่อนหน้านี้เรื่อง dependencies ระหว่าง LIFF App ของเรากับ LINE Platform นั้น Control ได้ยาก แต่ถ้าเราใช้ Cypress มาเขียนเทสแล้ว ปัญหานี้จะสามารถแก้ไขได้อย่างง่ายดายครับ

ที่ผมกำลังจะบอกก็คือว่า ผมกำลังจะทำให้ LIFF SDK ถูก Control Behaviorด้วย Cypress นั่นเองครับ

LIFF SDK is controlled by Cypress.io

LIFF App Testing with Cypress.io

มาดูกันครับ ว่าเราจะสามารถเขียน UI Test ให้กับ LIFF App ของเราด้วย Cypress ได้อย่างไร เริ่มต้นผมขอแนะนำ Feature หนึ่งที่น่าสนใจของ Cypress ก่อนนั่นก็คือ การทำ Stub นั่นเอง

ตัวอย่างการใช้ Stub บน Cypress

Stub คือการกำหนด Behavior ให้กับ function ที่เราต้องการจะ control ให้มันทำงานตามที่เรากำหนด โดยที่เราอาจจะกำหนดให้มัน return สิ่งที่เราต้องการออกมา (เราจะเรียกว่า Canned Answer) ซึ่งเราจะนำมา Apply ใช้กับ LIFF SDK โดยเราจะทำการ Stub LIFF Client API ทั้งหมดที่เราใช้งานใน LIFF App ของเราและสั่งให้มันทำงานโดยมี Behavior ตามที่ Test ของเราต้องการกันครับ

โดย Cypress Adopt การสร้าง Stub โดยใช้ Library ที่ชื่อว่า Sinon.js ซึ่งเป็นหนึ่งใน Javascript Library ที่ Specialize ด้านการสร้าง Stub มากๆ (เป็นหนึ่งในยุทธจักรเลยก็ว่าได้ ฮ่าๆ)

วิธีการ Stub LIFF Client API

ผมได้ลองสร้าง LIFF App ขึ้นมาโดยเขียนด้วย Vue.js และทำการเรียก liff.init() แบบนี้ครับ

ลองรัน Manual Test กับ LIFF App ของเราก่อน

ผมเขียน simple LIFF App ขึ้นมาอันหนึ่งเพื่อใช้ display ข้อมูลต่างๆ ของ user ง่ายๆ แบบนี้ครับ เริ่มต้นคือต้องให้ user ไปทำ LINE Login แล้วค่อยกลับมายัง LIFF App ของเราเพื่อแสดงผลข้อมูล User Profile รวมถึงผลการ call LIFF Client API บางตัวครับ

ทำให้ LIFF Client API ถูก Control ด้วย Cypress

Concept ของการที่จะทำให้ LIFF Client API นั้นถูก Control ด้วย Cypress ได้ก็คือ “เราจะต้องเช็คว่า LIFF App ของเราถูกรันด้วย Cypress หรือเปล่า ?ถ้าใช่ แทนที่จะใช้ LIFF SDK ปกติ ก็ให้ใช้ Stub LIFF Object ที่ Cypress จะ Define มาให้แทน”

การที่จะเช็คว่า LIFF App ของเราถูกรันด้วย Cypress หรือเปล่านั้นก็ง่ายมากครับ เมื่อแอพของเราถูกรันด้วย Cypress แล้วจะมี Object ของ Cypress ถูกฉีดใส่เข้าไปเพิ่มใน window object ของ browser ทำให้เราสามารถเช็คได้ด้วย Syntax แบบนี้ครับ

if (window.Cypress) {
// your app is running by Cypress, then use Stubbed LIFF SDK
window.liff = window.Cypress.liffMock;
}

สร้าง Stubbed LIFF Object ด้วย Cypress

ทีนี้มาถึงวิธีการสร้าง Stub LIFF Object ที่ Test Code ของเรากันนะครับ โดยที่ Cypress นั้นมี Command ที่ใช้ในการเข้าถึง Web App ของเราโดยใช้ syntax cy.visit(url, options?) เช่น cy.visit(‘https://myliffapp.dev’) เป็นต้น

และใน command นี้เราสามารถใส่ options เข้าไปได้ ซึ่งหนึ่งใน options ที่น่าสนใจก็คือการที่เราสามารถ hook onBeforeLoad event ได้ ซึ่ง event นี้จะถูกเรียกก่อนที่ App เราจะโหลดขึ้นมาใน browser และจะมีการ yield window object กลับมาให้เราสามารถ manipulate object ต่างๆ ที่ใช้ใน app ของเราได้ นั่นหมายความว่า เราสามารถ define Stubbed LIFF Object ได้ในจุดนี้ก่อนที่ LIFF App เราจะโหลดขึ้นมาพร้อมใช้งานนั่นเอง

การ stub LIFF Client API นั้นสามารถใช้ Cypress command cy.stub() ในการสร้าง stub function และเราสามารถกำหนด behavior ของ stub function ของเราได้หลากหลายรูปแบบมากๆ ครับ ยกตัวอย่างเช่น

  • cy.stub().resolves() ใช้สำหรับ stub function ที่ต้อง return Promise Resolve กลับมา ใช้ได้กับ LIFF Client API ที่เป็นลักษณะ asynchronous ทั้งหลาย เช่น liff.init(), liff.getProfile() เป็นต้นครับ
  • cy.stub().returns(value) ใช้สำหรับ stub function ที่เป็น synchronous และมีการ return ค่ากลับมาให้คนที่เรียกไปใช้งานต่อ เช่นถ้าเราต้องการ stub liff.isLoggedIn() ก็สามารถเรียกแบบนี้ได้ครับ cy.stub().as(‘isLoggedIn’).returns(true) ก็คือกำหนดให้เวลาที่เรียก liff.isLoggedIn() จะ return true เสมอ หรือจะเป็นการ simulate ว่าเหมือนแอพเรารันอยู่บนแอพ LINE บนมือถือ เราก็ทำการ stub liff.isInClient() เป็น cy.stub().as(‘isInClient’).returns(true) ได้เช่นกัน
  • cy.stub().callsFake(fakeFunction) ใช้สำหรับให้ stub function ไปเรียก fake function ที่เรากำหนดขึ้นมาแทน เช่น liff.openWindow() แทนที่เราจะปล่อยให้มันไปเปิด url บน browser จริงๆ เราก็สามารถให้มันมาเรียก fake function ของเราแทน ซึ่งใน fake function เราก็อาจจะทำ assertion ว่ามันมีการเรียก url ที่เราต้องการจริงๆ ด้วยนะ ตัด dependencies ไปได้เยอะเลย

เรายังสามารถสร้าง Stub ให้มัน return ค่าได้อีกหลากหลายแบบมากๆ เลยนะครับ สามารถหาอ่านเพิ่มเติมได้ที่ https://sinonjs.org/releases/latest/stubs/ เลยครับ

สร้าง Test Case สำหรับ LIFF App ด้วย Cypress

การสร้าง test case ด้วย Cypress นั้นง่ายมากครับ Cypress นั้นใช้ syntax การสร้าง test แบบเดียวกับ Mocha framework เลยเช่น describe, beforeEach, it ถ้าใครเคยเขียนเทสโดยใช้ keyword เหล่านี้มาแล้วก็สามารถนำมาใช้กับ Cypress ได้ทั้งหมดเลยครับ

Concept การเขียนเทสที่ดี

Concept การเขียนเทสทีดีนั้นก็มีอยู่ 3 ข้อครับ Given -> When -> Then ซึ่งถ้าเขียนเทสด้วย Cypress นั้นสามารถทำสิ่งที่ว่ามานี้ได้ภายใน 2–3 บรรทัดเท่านั้นครับ

  • Given: กำหนด Prerequisite ที่เทสเราต้องการเช่น cy.visit() เพื่อเข้าเว็บ, cy.stub() เพื่อสร้าง stub function ต่างๆ เป็นต้น
  • When: ทำการ Find view ที่เราต้องการจะเทสและทำ action กับมัน เช่นการกดปุ่มสามารถเขียนได้ในบรรทัดเดียวคือ cy.get(locator).click()
  • Then: ทำ assertion หลังจากเกิด action ใน step ก่อนหน้านี้ เช่น cy.get(@stub).should('be.calledOnce')

Cypress นั่นมีการ reset Stub data ให้เราอัตโนมัติหลังจากเทสแต่ละข้อรันจบนะครับ ดังนั้นไม่ต้องห่วงว่าจะมี dependencies ระหว่าง test case แต่ละข้อ ในกรณีที่มีการ stub function ที่แตกต่างกัน

ตอบคำถามปัญหาที่เราเคยเจอกับการเทส LIFF App ก่อนหน้านี้

  1. การทำ Integration Test กับ LINE Platform ทำให้เกิด Flaky Tests ได้ง่าย เราสามารถใช้ cy.stub() เพื่อ stub function liff.isLoggedIn() และจำลองว่า User ได้ทำการ Logged In เข้า LINE Platform เป็นที่เรียบร้อยแล้วได้ง่ายๆ เลย รวมถึง function อื่นๆ ของ LIFF Client API ก็สามารถ Control ได้ทั้งหมดแล้ว
LIFF Client API ถูก Control ด้วย Cypress ทั้งหมด

2. การจำลองการเทส LIFF App Testing บนแอพ LINE เอามาเทสบน Browser เป็นไปได้ยาก เราสามารถ stub liff.isInClient() เพื่อจำลองเหมือนว่าเรากำลังรัน LIFF App บน LINE ในมือถือได้ และ Cypress ก็ยังสามารถ Emulate การรันแอพบน Mobile Viewport ได้ด้วยคำสั่ง cy.viewport(viewport) เช่น cy.viewport(‘iphone-xr’) ได้เลยโดยที่เราไม่ต้องไปกำหนด screen resolution เอง มี preset มาให้เลือกใช้อยู่พอสมควรแล้วครับ นอกจากนี้เรายังสามารถ Config userAgent ที่เราต้องการใช้ในการรันแอพของเราผ่าน Configuration ของ Cypress ได้อีกด้วยครับ ก็ถือว่า Perfect เลยนะครับ เหมือนจริงมาก

Emulate mobile viewport ด้วย cy.viewport()

3. ไม่สามารถ Simulate Edge Cases หรือ Negative Tests ได้: ด้วยความสามารถของ Sinon.js ทำให้เราสามารถสร้าง Test Scenario ใหม่ๆ ที่เราไม่เคยคิดว่ามันจะ Automate ได้อย่างพวก Edge Cases หรือ Negative Tests ต่างๆ ได้แล้ว เช่นสมมติเราต้องการจะเทสว่าถ้า liff.init() แล้วเกิด error แล้วแอพเรา handle ได้ไหม เราก็สามารถสร้าง stub ให้ return Promise Reject ได้แบบนี้ครับ

ตรง init: cy.stub().as(‘initFailed’).rejects(liffInitError) คือการ stub ให้ liff.init() มีการ return Promise Reject กลับมานั่นเอง ดีงามสะดวกสบายสุดๆ ครับ

สามารถ Simulate Negative Case อย่าง LIFF Init Failed ได้อย่างง่ายๆ

การสร้าง Stub ที่ดี

การสร้าง Stub ใช่ว่าจะ Stub ยังไงก็ได้ตามใจเรานะครับ เราควรจะ Stub function ให้มี behavior ที่เหมือนจริงมากที่สุด เราควรไปอ่าน document ของ LIFF Client API แต่ละตัวว่ามันทำงานยังไง มีการรับค่า Argument และ Return ค่าออกมาเป็นอย่างไร และพยายาม Stub ให้เหมือนกับที่ document ได้เขียนไว้ให้ได้มากที่สุด และที่สำคัญอย่าลืมสร้าง Stub สำหรับทำ Negative Test ด้วยนะครับ

Cypress รันเทสเร็วแค่ไหน?

เนื่องจากว่า Cypress รันอยู่ใน run loop process เดียวกับแอพของเรา ด้วย Architecture แบบนี้จึงทำให้เทสของเราที่เขียนด้วย Cypress ใช้เวลาในการรันน้อยมากๆ ครับ (แต่ทั้งนี้ทั้งนั้นขึ้นอยู่กับวิธีการเขียนเทสของแต่ละคนด้วยนะครับ)

รันเทส LIFF App ด้วย Cypress 5 ข้อ ใช้เวลาแค่ 4.15 วินาทีเท่านั้น

Rerun Test ทุกครั้งที่มีการแก้โค้ดได้ไหม?

ถ้าใครที่ชอบทำ Test Driven Development (TDD) อาจจะอยากให้มีการ rerun test ทุกครั้งที่มีการแก้โค้ด ซึ่งถ้าเราเขียน test ด้วย Cypress นั้นจะมีการ Watch Code Changes ให้เราอัตโนมัติและทำการ Automatic Reload Test ของเราทันทีที่มีการแก้โค้ดให้เลยครับ

เมื่อแก้โค้ดแล้ว Test ก็ Auto Rerun Test ให้เลย ดีงามมากๆ

สรุป

ต้องบอกเลยว่า ปัญหาที่ผมเคยเจอนั้นถูกจัดการแก้ไขได้อย่างดีด้วย Stub Feature ของ Cypress และมันก็ทำให้เรามีความมั่นใจในการ release LIFF App ของเราขึ้น production มากขึ้น ใช้เวลาในการเทสแอพน้อยลงมากๆ ครับ หวังว่าบทความนี้จะเป็นประโยชน์กับนักพัฒนาและ QA ทุกๆ คนนะครับ

ถ้าใครที่สนใจอยากที่จะศึกษาเกี่ยวกับ Cypress.io เพิ่มเติม ตอนนี้เรามี Facebook Group Cypress.io Thailand สามารถเข้ามา Join ร่วมแลกเปลี่ยนความรู้ ประสบการณ์การใช้งานต่างๆ ได้เลยนะครับ และเราก็ยังมี Publication รวบรวมบทความที่เกี่ยวข้องกับ Cypress ในไทยไว้ทั้งหมด กด Follow กันได้ครับ Happy Testing!

--

--