Mengapa Unit Test Sering Gagal Menangkap Regresi Visual
Unit test dan end-to-end (E2E) functional test memiliki batasan fundamental: keduanya memverifikasi logika dan pohon komponen (tree structure), bukan representasi raster piksel di layar. Sebuah pengujian assertion seperti expect(screen.getByText('Checkout')).toBeVisible() tetap bernilai true meskipun elemen tersebut memiliki opacity: 0.01, tertutup absolute-positioned modal dengan z-index salah, atau teksnya terpotong (truncated) akibat pembaruan token spasi flexbox.
UI drift terjadi saat perubahan kecil pada style sheets, dependency upgrades (seperti migrasi React Native core atau Yoga engine), atau perbedaan resolusi menyebabkan layout bergeser tanpa memicu runtime error. Visual regression testing berbasis pixel diff menyelesaikan masalah ini dengan membandingkan screenshot hasil render komponen atau layar secara bitwise/pixelwise terhadap gambar acuan (baseline snapshot).
Arsitektur Komparasi: Memilih Tooling Pixel Diff
Dua pendekatan paling umum dalam ekosistem React Native adalah pengujian berbasis emulasi native penuh dan pengujian rendering statis:
- React Native Owl: Menjalankan aplikasi langsung pada iOS Simulator atau Android Emulator, menangkap screenshot native sebenarnya, lalu melakukan diff menggunakan
pixelmatch. Cocok untuk pengujian high-fidelity yang melibatkan komponen native seperti Navigation, Map, atau Kamera. - Jest Image Snapshot: Dikombinasikan dengan render engine berbasis web/DOM (seperti React Native for Web) atau output skia. Lebih cepat dan hemat resource di CI, tetapi tidak dapat mendeteksi bug rendering spesifik platform native (misalnya layouting bayangan iOS vs Android elevation).
Untuk akurasi visual native tertinggi, arsitektur berbasis emulator/simulator dengan perbandingan piksel langsung adalah standar industri.
Membangun Deterministic Rendering Environment
Tantangan terbesar visual regression testing adalah flakiness (ketidakkonsistenan). Perbedaan 1 piksel saja akibat font anti-aliasing, animasi yang belum selesai, atau status bar clock akan menggagalkan seluruh test suite. Renderer harus dibuat sepenuhnya deterministik.
1. Mematikan Animasi Native dan Transisi
Animasi berbasis driver native atau Reanimated harus dimatikan atau dipaksa berjalan dengan durasi 0. Di Android Emulator, matikan skala animasi sistem sebelum mengeksekusi test runner:
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0Pada sisi JavaScript, mock konfigurasi transisi React Navigation atau set flag global pengujian:
// jest.setup.js atau test-environment-setup.ts
import { UIManager } from 'react-native';
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(false);
}
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');2. Mock Timestamp, Timezone, dan Locale
Komponen yang menampilkan tanggal, waktu, atau mata uang harus selalu menggunakan nilai statis. Gunakan mock global pada JavaScript runtime:
const MOCK_DATE = new Date('2025-01-15T12:00:00.000Z');
global.Date = class extends Date {
constructor(...args) {
if (args.length > 0) {
super(...args);
} else {
super(MOCK_DATE);
}
}
static now() {
return MOCK_DATE.getTime();
}
};3. Masking Status Bar dan Safe Area
Status bar baterai, jam, dan notch perangkat menghasilkan variasi piksel yang tidak relevan dengan kode aplikasi. Lakukan masking atau sembunyikan status bar sepenuhnya:
import { StatusBar } from 'react-native';
// Sembunyikan status bar sebelum menangkap snapshot
StatusBar.setHidden(true, 'none');Implementasi Test Script dan Konfigurasi Runner
Berikut adalah konfigurasi react-native-owl melalui owl.config.json untuk mengontrol toleransi perbandingan dan direktori snapshot:
{
"ios": {
"simulator": "iPhone 15",
"build": "xcodebuild -workspace ios/App.xcworkspace -scheme App -configuration Release -destination 'platform=iOS Simulator,name=iPhone 15'"
},
"android": {
"device": "Pixel_7_API_34",
"build": "cd android && ./gradlew assembleRelease"
},
"images": {
"baseline": "./visual-baselines",
"diff": "./visual-diffs",
"threshold": 0.05
}
}Contoh skenario pengujian komponen antarmuka menggunakan Owl:
import { takeScreenshot } from 'react-native-owl';
describe('PaymentCard Component Visual Check', () => {
it('harus merender state default tanpa layout drift', async () => {
// Render target screen/view
await element(by.id('nav-to-payment-screen')).tap();
await waitFor(element(by.id('payment-card'))).toBeVisible();
// Ambil screenshot layar saat ini dan komparasi dengan baseline
const screenshot = await takeScreenshot('payment_card_default');
expect(screenshot).toPassImageRatio(0.002); // 0.2% max pixel difference
});
});Tuning Threshold Toleransi Piksel untuk CI Runner
Di lingkungan Continuous Integration (CI), kartu grafis (GPU) sering kali digantikan oleh CPU software rasterizer (misalnya Mesa di Linux atau virtual display di macOS GitHub Runner). Hal ini menyebabkan perbedaan kecil pada sub-pixel font rendering dan color blending.
Penting: Jangan menyetel toleransi ke 0% kecuali runner lokal dan runner CI menggunakan perangkat keras, driver grafis, dan image OS yang identik.
Ada dua parameter utama dalam tuning diffing engine:
- Threshold per Pixel (Sensitivity): Nilai antara 0.0 hingga 1.0 (sering kali bernilai 0.1 di Pixelmatch). Menentukan seberapa besar delta warna (YIQ atau RGBA) sebelum sebuah piksel dianggap berbeda.
- Failure Threshold Ratio (Toleransi Keseluruhan): Persentase total piksel yang diizinkan berbeda terhadap total resolusi gambar. Gunakan nilai
0.001hingga0.005(0.1% - 0.5%) untuk menyerap perbedaan rasterizer tanpa melewatkan tombol yang hilang atau teks yang terpotong.
Workflow Manajemen Baseline Snapshot via Git LFS
Menyimpan screenshot berukuran penuh (PNG 32-bit) secara langsung di riwayat Git biasa menyebabkan ukuran repositori membengkak secara eksponensial setiap kali baseline diperbarui. Gunakan Git Large File Storage (LFS).
Inisialisasi Git LFS untuk folder baseline visual:
git lfs install
git lfs track "visual-baselines/**/*.png"
git add .gitattributesWorkflow Update Baseline
Ketika perubahan desain disetujui (misalnya ada rebranding atau perubahan border-radius yang disengaja), lakukan workflow pembaruan berikut:
- Jalankan runner dengan flag pembaruan baseline:
npx owl update-baseline --platform ios. - Review visual diff yang dihasilkan secara lokal untuk memastikan tidak ada efek samping di luar scope perubahan.
- Commit baseline PNG baru ke Git LFS bersamaan dengan perubahan kode komponen:
git commit -m "chore(visual): update baseline snapshot checkout card".
Integrasi Pipeline GitHub Actions
Contoh implementasi pipeline CI macOS runner untuk menjalankan visual regression test iOS:
name: Visual Regression Tests
on:
pull_request:
paths:
- 'src/**'
- 'ios/**'
- 'visual-baselines/**'
jobs:
visual-test:
runs-on: macos-14
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
lfs: true
- name: Setup Git LFS
run: git lfs checkout
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Run Owl Visual Tests
run: yarn owl test --platform ios
- name: Upload Diff Artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-diff-reports
path: ./visual-diffs
retention-days: 7Jika terjadi kegagalan pada tahap Run Owl Visual Tests, pipeline secara otomatis mengunggah artefak gambar diff ke GitHub Summary. Engineer dapat mengunduh dan menganalisis secara presisi koordinat piksel mana yang mengalami pergeseran sebelum me-merge PR ke branch utama.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!