มาลองใช้ Visual Testing บน Cypress เครื่องมือที่ช่วยให้การทดสอบ UI ง่ายขึ้นกัน

PANRUETHAI EAMPHICHIT
Tri Petch Digital
Published in
5 min readMar 31, 2024

หลายๆคนคงเคยลองใช้ Automated Testing มาช่วยในการทดสอบแบบ Functional Testing กันมาบ้างแล้ว แต่สำหรับหน้าเว็บบางประเภทที่เราต้องการตรวจสอบความถูกต้องของ UI โดยใช้การทดสอบแบบ Visual Testing ตัว Cypress เองเขาก็มี Open source plug-in มากมายให้เราเลือกใช้ ลองเข้าไปดูกันได้ที่นี่เลยค่ะ -> (https://docs.cypress.io/plugins#visual-testing)

ในที่นี้เราจะขอยกตัวอย่างหนึ่งใน Library Visual Testing ที่เราลองใช้กับ Project ของเราแล้วเวิร์คมากๆมาแนะนำให้เพื่อนๆได้รู้จักกัน โดย Library นี้มีชื่อว่า cypress-image-diff ค่ะ

ขั้นตอนการ install และ set up

  • Install package และ report html
npm i -D cypress-image-diff-js cypress-image-diff-html-report
  • Setup Cypress Plugin
// cypress.config.js
const { defineConfig } = require("cypress");
const getCompareSnapshotsPlugin = require('cypress-image-diff-js/plugin');


module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
return getCompareSnapshotsPlugin(on, config);
},
},
});
Setup Cypress Support
// cypress/support/{scheme}.js, where {scheme} defaults to e2e
const compareSnapshotCommand = require('cypress-image-diff-js/command');

หลักการทำงานของ library cypress-image-diff

เป็นการ Compare Screen ระหว่าง Baseline Screen และ Actual Screen

Baseline Screen: การที่เราจะ Compare Screen ได้ เราจะต้องมี Screen ที่เป็นตัวตั้งต้นหรือ Expect Result ก่อน ซึ่ง Screen นั้นจะเรียกว่า Baseline Screenโดยในการ Run เก็บ Screen ครั้งแรก จะเป็นการเก็บ Screen นั้นไว้เป็น Baseline ให้อัตโนมัติ ใน Folder ที่เรากำหนดไว้

Actual Screen: หลังจากที่เราได้ Baseline Screen มาแล้ว ในการ Run Test ในครั้งถัดๆไป จะเป็นการเก็บ Actual Screen เพื่อมา Compare กับ Baseline ค่ะ

แล้วถ้าวันนึง UI เราเปลี่ยนล่ะจะทำอย่างไร?
Ans. ถ้ามีการเปลี่ยน UI ให้ทำการลบไฟล์ Screen Baseline นั้นทิ้งไป แล้วทำการ Run เก็บ Screen ใหม่ จะเป็นการเก็บ Screen นั้นๆเป็น Baseline ให้ใหม่ค่ะ

เก็บ Baseline และ Actual Screen ไว้ที่ไหน?
Ans. Screen ที่ระบบได้ทำการ Capture มานั้นจะถูกเก็บไว้ใน Folder Root ของ Project เป็น Default

โดย Folder Structure ของ Library นี้ ประกอบไปด้วย

  • cypress-image-diff-html-report: สำหรับเก็บ Report
  • cypress-image-diff-screenshots: สำหรับเก็บ Screen Baseline, Actual, Different
Folder Structure

อ่านบทความมาถึงตรงนี้แล้วพอจะเข้าใจ Concept ของ Library ตัวนี้กันแล้วใช่ไหมคะ งั้นเรามาลองใช้งานกันเลยดีกว่าค่ะ

Command ในการเก็บ Screen สามารถเก็บได้หลายแบบ ดังนี้

  1. เก็บทั้ง Page
cy.compareSnapshot('filename');

2. เก็บเฉพาะ Element

cy.get('element').compareSnapshot('filename');

เมื่อทำการ Run แล้วจะได้ Screen ที่แตกต่างกันตาม Command ที่เราใช้ตามรูปด้านล่างค่ะ

ตัวอย่าง Capture Screen ทั้งแบบ Page และแบบ Element
ตัวอย่าง Capture Screen แบบเก็บทั้ง Page และเก็บเฉพาะ Element

Report

หลังจาก Run test ได้แล้ว ตัว Library นี้เขาก็มี Report ให้เราดูได้ด้วยค่ะว่า ผลการ Run test นั้น Pass หรือ Fail และถ้า Fail Fail ตรงจุดไหน ตามภาพด้านล่างนี้เลยค่ะ

  • รูปซ้าย: เป็น Baseline Screen
  • รูปกลาง: เป็นผลการ Compare ระหว่าง Baseline vs Actual Screen โดยถ้ามีจุดที่ Different กันจะแสดงจุดนั้นเป็นสีแดง
  • รูปขวา: เป็น Actual Screen ที่ Capture ได้ในการ Run test รอบนั้นๆ
Report ผลการ Compare Screen

สรุป

อย่างไรก็ตาม หลังจากที่เราได้ลองใช้ library ตัวนี้ แล้วขอสรุปข้อดีและข้อจำกัดในการใช้ Library ตัวนี้ ดังนี้

ข้อดี

  • Visual Regression Test: สามารถช่วยตรวจสอบความถูกต้องของการแสดงผล UI โดยการเปรียบเทียบความแตกต่างระหว่าง Baseline และ Actual Screen
  • Easy to Use: Command เข้าใจง่ายและใช้งานง่าย
  • Configurable Threshold: สามารถกำหนดค่า Thresholds ระดับความแตกต่างที่ยอมรับได้ ซึ่งช่วยในการปรับความไวของการเปรียบเทียบความแตกต่างระหว่าง Baseline และ Actual Screen
  • Report: มี Report ที่ละเอียดและเข้าใจง่าย ใช้สำหรับตรวจสอบผลการทดสอบได้
  • Active Community Support: มี Community ที่สามารถถาม/ตอบได้และมีผู้ช่วยให้คำแนะนำและการช่วยเหลือในการใช้งานตลอดเวลา

ข้อจำกัดในการใช้ Library

  • Same Infrastructure : มีข้อจำกัดในการ Run เช่น ถ้าเก็บ Baseline ที่ MAC OS จะต้องทำการ Run test ที่ MAC OS เท่านั้น ถ้ารันบน Window การ Compare อาจจะคลาดเคลื่อนทำให้เกิด Different ได้
  • ข้อจำกัดในการทำ Visual Testing เช่น การตรวจสอบความเปลี่ยนแปลงที่เกิดจากการแก้ไขข้อความหรือการมี animation บนหน้าเว็บ อาจจะต้อง disable การเล่น animation ออกไปเพื่อให้หน้าจอมีความนิ่งก่อนที่จะสั่ง Capture ได้

แนะนำวิธีแก้ไขปัญหาของข้อจำกัดของ library cypress-image-diff

Solution1: ข้อจำกัดในเรื่องของการที่ต้องใช้ Infrastructure เดียวกันทั้งในตอนเก็บ Baseline และตอน Run test

ทางผู้พัฒนา Library ตัวนี้ เขาก็ได้แนะนำวิธีแก้ไขปัญหานี้มาให้ 2 วิธี คือ

  • Run test บน Docker Container
  • Run test โดยใช้ Cloud Browser เช่น BrowserStack

เราเลือกใช้วิธีแรกค่ะคือ Run test บน Docker เพราะตัว Cypress เองเขามี Official Image ให้เราเลือกใช้ได้ฟรีหลายตัวเลย ทำให้วิธีนี้ง่ายมากๆค่ะ

ขั้นตอนในการ Build Image สำหรับ Run test

  1. สร้าง Dockerfile โดยเลือกใช้ตาม Official Image ที่ Link นี้ค่ะ-> (https://docs.cypress.io/examples/docker)
    จากตัวอย่างของเรา เราจะเลือกสร้าง Container จาก cypress/factory โดยเราสามารถกำหนด Version ของ Cypress, Yarn, Browser ต่างๆได้
# Args are defined in the Dockerfile before the FROM command.
# Using these args will cause an image to be created with node (default version is 16.18.1), chrome, firefox and edge.
ARG NODE_VERSION='18.14.0'
ARG YARN_VERSION='1.22.19'
ARG CYPRESS_VERSION='13.0.0'
ARG CHROME_VERSION='112.0.5615.121-1'
ARG EDGE_VERSION='112.0.1722.48-1'
ARG FIREFOX_VERSION='112.0'

FROM cypress/factory

COPY . /opt/app
WORKDIR /opt/app

RUN npm install --save-dev cypress

# Read Thai
RUN apt-get update --allow-releaseinfo-change && \
DEBIAN_FRONTEND=noninteractive apt-get install -y locales fonts-thai-tlwg-ttf && \
sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=en_US.UTF-8

ENV LANG en_US.UTF-8
SHELL ["/bin/bash", "-c"]

2. สร้าง Image จาก Dockerfile โดยการ Run command ด้านล่างนี้ค่ะ (Run ใน directory ที่เก็บ Dockerfile)

docker build -t {image_tagname} .

3. ติดตั้งโปรแกรม XQuartz เพื่อให้สามารถเปิดโปรแกรม GUI บน Docker ได้
เมื่อติดตั้งเสร็จแล้วให้ตั้งค่าตามรูปด้านล่างนี้ แล้วทำการ Logout เครื่องสัก 1 รอบค่ะ
Config: Preferences… -> Security Tab -> Allow network connection

ตั้งค่า Allow Security บนโปรแกรม XQuartz

4. เปิดโปรแกรม XQuartz และทำการ Run Command ด้านล่าง เพื่อ Set Display Environment และเพิ่ม IP with xhost ค่ะ

export DISPLAY=$IP:0
IP=$(ifconfig en0 | grep inet | awk '$1=="inet" {print $2}')
/usr/X11/bin/xhost + $IP

ถ้า Run สำเร็จจะมี “{IP} being added to access control list” ขึ้นมาดังรูปค่ะ

5. ทำการ Run Docker with XQuartz ขึ้นมาด้วย command ด้านล่างนี้ค่ะ

docker run -it \
-v $PWD:/{project-directory} \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v ~/.Xauthority:/.Xauthority \
-e DISPLAY=$IP:0 \
-e XAUTHORITY=/.Xauthority \
--net host \
-w /{project-directory} {image_tagname}

ลองใช้ Command check ว่า Docker ของเรา Run ขึ้นมาแล้วหรือไม่

docker ps

จะเห็น Docker {image_tagname} ของเราแสดงขึ้นมาค่ะ

หลังจากที่เรา Run Docker ขึ้นมาสำเร็จแล้ว ให้เราทำการลบ node_modules และทำการรัน install packages ใหม่ทั้งหมดด้วย Command ด้านล่างอีกครั้ง

yarn --frozen-lockfile

เมื่อ install เสร็จเรียบร้อย ก็สามารถ Run Cypress บน Docker เสมือนรันบนเครื่อง local ปกติได้เลยค่ะ ในการ Run Test ครั้งถัดไปเราก็แค่ทำตาม Step เดิมได้เลย คือ

  • เปิด XQuartz
  • Run Docker with XQuartz
  • Run Cypress

Solution2: ข้อจำกัดในกรณีที่เว็บมีรูปหรือanimation

  1. ในกรณีที่หน้าเว็บมีรูปภาพ เราอาจจะต้องเช็กก่อนว่ารูปถูกโหลดสำเร็จแล้วหรือยังก่อนที่จะ Capture
    วิธีคือเราจะต้องให้ Developer ช่วยเพิ่ม attribute หรือ class ออกมาใน tag html เมื่อรูปภาพทำการ load สำเร็จแล้ว ตัวอย่างเช่น

โดยเราสามารถเขียน Script เพื่อ Check ว่ารูปโหลดสำเร็จแล้วจึงค่อยทำการสั่ง Capture แบบตัวอย่างด้านล่างนี้ได้เลยค่ะ

cy.get('[data-test="content_item_image"]').should('have.class', 'is-load');
cy.compareSnapshot('WholePage');

2. ในกรณีที่หน้าเว็บเรามีการเล่น animation เราจะต้องทำการ disable การเล่น animation บนเว็บออกไปก่อนที่จะ Capture
วิธีคือเราจะต้องให้ Developer ช่วยเพิ่ม attribute เพื่อไม่ให้เว็บทำการเล่น animation ตัวอย่างเช่น

โดยเราสามารถเขียน Script Remove attribute data-cy=”animate” เพื่อไม่ให้เว็บทำการเล่น animationได้ แบบตัวอย่างด้านล่างนี้ได้เลยค่ะ

cy.get('[data-cy="animate"]')
.invoke('removeAttr', 'data-cy', 'animate')
.should('not.have.attr', 'data-cy', 'animate');
cy.compareSnapshot('WholePage');

แค่นี้ animation หรือ รูป ก็ไม่เป็นอุปสรรคในการ Capture ของเราแล้วค่ะ

--

--