1. Pengenalan Cypress
Cypress adalah framework testing end-to-end (E2E) open-source yang dibangun khusus untuk web modern. Dikembangkan oleh Cypress.io, tool ini berjalan langsung di browser (bukan Selenium) sehingga memberikan pengalaman testing yang lebih cepat, andal, dan mudah di-debug.
Berbeda dengan Selenium WebDriver yang mengirim perintah melalui jaringan, Cypress berjalan di dalam event loop yang sama dengan aplikasi Anda. Ini memberikan kontrol penuh terhadap browser, network, dan DOM — menghasilkan test yang lebih stabil dan tidak flaky.
Keunggulan Cypress
| Keunggulan | Penjelasan |
|---|---|
| Time Travel | Screenshot otomatis di setiap langkah — lihat state saat test berjalan |
| Real-time Reload | Test otomatis re-run saat file diubah |
| Debugging Mudah | Gunakan DevTools langsung, error messages yang jelas |
| Automatic Waiting | Tidak perlu manual waits/polling — Cypress menunggu otomatis |
| Network Stubbing | Intercept dan mock HTTP requests dengan mudah |
| Screenshots & Videos | Rekam video dan screenshot otomatis saat test gagal |
| Cross-browser | Chrome, Firefox, Edge, Electron |
| Developer Experience | API yang intuitif, dokumentasi luar biasa |
Cypress vs Selenium vs Playwright
| Aspek | Cypress | Selenium | Playwright |
|---|---|---|---|
| Architecture | In-browser | WebDriver | CDP Protocol |
| Bahasa | JavaScript | Multi-language | Multi-language |
| Speed | 🟢 Cepat | 🟡 Sedang | 🟢 Cepat |
| Debugging | 🟢 Sangat baik | 🟡 Dasar | 🟢 Trace Viewer |
| Parallel | 🟡 Perlu Dashboard | 🟢 Bawaan | 🟢 Bawaan |
| iFrame Support | 🔴 Terbatas | 🟢 Baik | 🟢 Baik |
| Learning Curve | 🟢 Mudah | 🟡 Sedang | 🟢 Mudah |
┌──────────────────────────────────────────────────────┐ │ CYPRESS TEST RUNNER │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ Browser (Chrome/Firefox) │ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────────────┐ │ │ │ │ │ Your Web │ │ Cypress Runner │ │ │ │ │ │ Application │◄──►│ (Same origin) │ │ │ │ │ │ │ │ │ │ │ │ │ │ DOM Events │ │ • cy.get() │ │ │ │ │ │ Network │ │ • cy.click() │ │ │ │ │ │ Cookies │ │ • cy.intercept() │ │ │ │ │ └──────────────┘ │ • Assertions │ │ │ │ │ └──────────────────────┘ │ │ │ └────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ Node.js Server │ │ │ │ • File system access │ │ │ │ • Task execution │ │ │ │ • Plugin hooks │ │ │ └────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────┘
2. Instalasi & Setup
Instalasi
# Instal Cypress sebagai dev dependency npm install cypress --save-dev # Atau dengan yarn yarn add cypress --dev # Atau dengan pnpm pnpm add cypress -D # Buka Cypress Test Runner (GUI) npx cypress open # Jalankan test headless (untuk CI) npx cypress run # Jalankan test untuk browser tertentu npx cypress run --browser chrome npx cypress run --browser firefox
Struktur Folder Cypress
project-root/ ├── cypress/ │ ├── e2e/ ← Test files (specs) │ │ ├── auth/ │ │ │ ├── login.cy.js │ │ │ └── register.cy.js │ │ ├── dashboard/ │ │ │ └── dashboard.cy.js │ │ └── homepage.cy.js │ ├── fixtures/ ← Test data (JSON) │ │ ├── users.json │ │ ├── products.json │ │ └── api-responses.json │ ├── support/ ← Custom commands & utilities │ │ ├── commands.js ← Custom commands │ │ ├── e2e.js ← Global config (beforeEach, dll.) │ │ └── helpers.js ← Utility functions │ └── downloads/ ← Downloaded files during tests ├── cypress.config.js ← Cypress configuration └── package.json
Konfigurasi Cypress
const { defineConfig } = require('cypress');
module.exports = defineConfig({
// E2E testing config
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/e2e.js',
// Timeouts
defaultCommandTimeout: 10000, // 10 detik
requestTimeout: 15000, // 15 detik untuk cy.request()
responseTimeout: 30000, // 30 detik untuk server response
pageLoadTimeout: 60000, // 60 detik untuk page load
// Screenshots & Videos
screenshotOnRunFailure: true,
video: true,
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
// Viewport
viewportWidth: 1280,
viewportHeight: 720,
// Retries (berguna di CI)
retries: {
runMode: 2, // Retry 2x saat run headless (CI)
openMode: 0, // Tidak retry saat open (development)
},
// Experimental features
experimentalStudio: true,
// Setup node events (plugins)
setupNodeEvents(on, config) {
// Implementasi plugin di sini
on('task', {
// Custom task untuk seed database
async seedDatabase() {
// Implementasi seed...
return null;
},
// Log ke terminal
log(message) {
console.log(message);
return null;
},
});
return config;
},
},
});
3. Commands: Perintah Dasar
Cypress menggunakan chained commands yang membaca seperti kalimat bahasa Inggris. Setiap command mengembalikan elemen yang bisa di-chain ke command berikutnya.
Querying Elements
describe('Querying Elements', () => {
beforeEach(() => {
cy.visit('/halaman-produk');
});
it('berhasil menemukan elemen dengan berbagai selector', () => {
// ═══ cy.get() — Selector utama ═══
cy.get('.judul-produk'); // CSS class
cy.get('#produk-1'); // ID
cy.get('[data-cy="add-to-cart"]'); // Data attribute (recommended!)
cy.get('button'); // Tag name
cy.get('[type="submit"]'); // Attribute selector
cy.get('.kartu h3'); // Descendant selector
// ═══ cy.contains() — Cari berdasarkan teks ═══
cy.contains('Tambah ke Keranjang');
cy.contains('button', 'Beli'); // Elemen tertentu + teks
cy.contains(/harga/i); // Regex
// ═══ cy.find() — Cari di dalam elemen yang sudah dipilih ═══
cy.get('.product-card')
.find('.harga');
// ═══ Within a specific area ═══
cy.get('.sidebar')
.within(() => {
cy.get('a').should('have.length.gte', 3);
cy.contains('Settings').click();
});
});
});
Actions (User Interactions)
describe('User Actions', () => {
beforeEach(() => {
cy.visit('/form-demo');
});
it('berhasil mengisi dan mengirim form', () => {
// ═══ cy.type() — Ketik teks ═══
cy.get('[data-cy="input-nama"]')
.type('Budi Santoso')
.should('have.value', 'Budi Santoso');
cy.get('[data-cy="input-email"]')
.type('budi@example.com');
// Type dengan opsi khusus
cy.get('[data-cy="input-password"]')
.type('Rahasia123!', { delay: 50 }); // delay antar karakter
// ═══ cy.clear() — Hapus input ═══
cy.get('[data-cy="input-search"]')
.clear()
.type('query baru');
// ═══ cy.click() — Klik elemen ═══
cy.get('[data-cy="btn-submit"]').click();
cy.get('.card').first().click();
cy.get('.card').last().click();
cy.get('.card').eq(2).click(); // Index ke-3
// Double click
cy.get('[data-cy="editable"]').dblclick();
// Right click
cy.get('[data-cy="context-menu"]').rightclick();
// ═══ cy.select() — Dropdown select ═══
cy.get('[data-cy="select-kategori"]')
.select('elektronik'); // Berdasarkan value
cy.get('[data-cy="select-kota"]')
.select('Jakarta'); // Berdasarkan text
// ═══ cy.check() / cy.uncheck() — Checkbox ═══
cy.get('[data-cy="checkbox-setuju"]')
.check()
.should('be.checked');
cy.get('[data-cy="checkbox-setuju"]')
.uncheck()
.should('not.be.checked');
// Check multiple checkboxes
cy.get('.checkbox-group input')
.check(['react', 'vue', 'angular']);
// ═══ Radio buttons ═══
cy.get('[data-cy="radio-gold"]')
.check()
.should('be.checked');
// ═══ Scroll ═══
cy.scrollTo('bottom');
cy.get('#footer').scrollIntoView();
// ═══ File upload ═══
cy.get('[data-cy="file-input"]')
.selectFile('cypress/fixtures/test-image.png');
// Drag and drop
cy.get('[data-cy="drag-source"]')
.drag('[data-cy="drop-target"]');
});
});
Navigation & Window
describe('Navigation', () => {
it('berhasil navigasi halaman', () => {
// Kunjungi halaman
cy.visit('/');
cy.visit('/dashboard');
cy.visit('http://localhost:3000/profile');
// Visit dengan auth headers
cy.visit('/admin', {
headers: {
Authorization: 'Bearer token123',
},
});
// Navigasi browser
cy.go('back');
cy.go('forward');
cy.reload();
// URL assertions
cy.url().should('include', '/dashboard');
cy.url().should('eq', 'http://localhost:3000/dashboard');
cy.location('pathname').should('eq', '/dashboard');
// Cookies
cy.setCookie('session', 'abc123');
cy.getCookie('session').should('have.property', 'value', 'abc123');
cy.clearCookies();
// Local Storage
cy.window().then((win) => {
win.localStorage.setItem('token', 'xyz789');
});
cy.clearLocalStorage();
// Title
cy.title().should('eq', 'Dashboard - BeebaneLabs');
});
});
4. Assertions
Cypress menggunakan Chai BDD assertions secara bawaan, plus Sinon untuk spies/stubs. Assertions ditulis secara natural dan mendukung chaining.
Chai & Chai-jQuery Assertions
describe('Assertions', () => {
beforeEach(() => {
cy.visit('/dashboard');
});
it('berhasil menggunakan berbagai assertion', () => {
// ═══ Implicit Assertions (should) ═══
// Visibility & Existence
cy.get('[data-cy="judul"]').should('be.visible');
cy.get('[data-cy="hidden-element"]').should('not.be.visible');
cy.get('[data-cy="card"]').should('exist');
cy.get('[data-cy="nonexistent"]').should('not.exist');
// Text content
cy.get('[data-cy="judul"]')
.should('have.text', 'Dashboard Utama'); // Exact match
cy.get('[data-cy="judul"]')
.should('contain.text', 'Dashboard'); // Partial match
cy.get('[data-cy="judul"]')
.should('match', /dashboard/i); // Regex
// CSS & Classes
cy.get('[data-cy="card"]')
.should('have.class', 'active');
cy.get('[data-cy="card"]')
.should('have.css', 'color', 'rgb(255, 255, 255)');
cy.get('[data-cy="card"]')
.should('have.css', 'background-color')
.and('not.eq', 'rgb(0, 0, 0)');
// Attributes
cy.get('[data-cy="link"]')
.should('have.attr', 'href', '/profile');
cy.get('[data-cy="input"]')
.should('have.attr', 'placeholder', 'Cari...')
.and('have.attr', 'required');
// Value (for inputs)
cy.get('[data-cy="search"]')
.type('query')
.should('have.value', 'query');
// Length & Count
cy.get('.product-card')
.should('have.length.gte', 1); // >= 1
cy.get('.product-card')
.should('have.length', 6); // Exactly 6
cy.get('.product-card')
.should('have.length.lte', 10); // <= 10
// Chain multiple assertions
cy.get('[data-cy="card"]')
.should('be.visible')
.and('have.class', 'card')
.and('contain.text', 'Produk');
// ═══ Explicit Assertions ═══
cy.get('[data-cy="harga"]').then(($el) => {
// Chai expect
expect($el.text()).to.equal('Rp 150.000');
expect($el).to.have.class('harga');
expect($el).to.be.visible;
});
// Assert on subject
cy.get('[data-cy="count"]').invoke('text').then((text) => {
expect(parseInt(text)).to.be.greaterThan(0);
expect(parseInt(text)).to.be.lessThan(100);
});
});
});
5. Fixtures & Data Testing
Fixtures adalah file data (biasanya JSON) yang digunakan untuk test data yang konsisten. Ini memungkinkan test Anda tidak bergantung pada data production atau state database.
Membuat dan Menggunakan Fixtures
{
"admin": {
"nama": "Admin Beebane",
"email": "admin@beebane.com",
"password": "Admin123!",
"role": "admin"
},
"user": {
"nama": "Budi Santoso",
"email": "budi@example.com",
"password": "User123!",
"role": "user"
},
"premium": {
"nama": "Sari Dewi",
"email": "sari@example.com",
"password": "Premium123!",
"role": "premium"
},
"invalid": {
"email": "salah@example.com",
"password": "password_salah"
}
}
{
"products": [
{
"id": 1,
"nama": "Laptop ASUS ROG",
"harga": 15000000,
"kategori": "elektronik",
"stok": 25
},
{
"id": 2,
"nama": "Keyboard Mechanical",
"harga": 750000,
"kategori": "aksesoris",
"stok": 100
},
{
"id": 3,
"nama": "Monitor 4K 27 inch",
"harga": 5500000,
"kategori": "elektronik",
"stok": 0
}
]
}
describe('Menggunakan Fixtures', () => {
// Load fixture sebelum semua test
beforeEach(() => {
// Load dan alias fixture
cy.fixture('users').as('usersData');
cy.fixture('products').as('productsData');
});
it('login dengan data dari fixture', function () {
// Akses data via alias (gunakan function, BUKAN arrow function!)
const { email, password } = this.usersData.user;
cy.visit('/login');
cy.get('[data-cy="input-email"]').type(email);
cy.get('[data-cy="input-password"]').type(password);
cy.get('[data-cy="btn-login"]').click();
cy.url().should('include', '/dashboard');
});
it('menampilkan produk dari fixture', function () {
const { products } = this.productsData;
cy.visit('/produk');
products.forEach((product, index) => {
cy.get('.product-card')
.eq(index)
.should('contain.text', product.nama)
.and('contain.text', `Rp ${product.harga.toLocaleString('id-ID')}`);
});
});
// Atau load fixture langsung di test
it('gunakan cy.intercept dengan fixture', () => {
cy.fixture('products').then((data) => {
// Mock API response
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: data.products,
}).as('getProducts');
});
cy.visit('/produk');
cy.wait('@getProducts');
cy.get('.product-card').should('have.length', 3);
});
});
6. Intercepting API Requests
cy.intercept() memungkinkan Anda menangkap, memodifikasi, dan mock network requests. Ini sangat penting untuk E2E testing karena memungkinkan Anda mengisolasi frontend dari backend.
Intercept Patterns
describe('API Intercepting', () => {
it('mock API response', () => {
// Mock GET request
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, nama: 'Budi' },
{ id: 2, nama: 'Sari' },
],
}).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.get('.user-card').should('have.length', 2);
});
it('mock error response', () => {
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { error: 'Server Error' },
}).as('getProductsError');
cy.visit('/products');
cy.wait('@getProductsError');
cy.get('.error-message')
.should('be.visible')
.and('contain.text', 'Terjadi kesalahan');
});
it('intercept dan verifikasi request', () => {
// Intercept POST request dan verifikasi body-nya
cy.intercept('POST', '/api/orders').as('createOrder');
cy.visit('/checkout');
cy.get('[data-cy="btn-order"]').click();
cy.wait('@createOrder').then((interception) => {
// Verifikasi request body
expect(interception.request.body).to.have.property('items');
expect(interception.request.body.items).to.have.length.greaterThan(0);
// Verifikasi response
expect(interception.response.statusCode).to.eq(201);
});
});
it('modifikasi request sebelum dikirim', () => {
// Tambahkan header ke request
cy.intercept('GET', '/api/data', (req) => {
req.headers['Authorization'] = 'Bearer mock-token';
req.continue(); // Lanjutkan request dengan modifikasi
}).as('getData');
cy.visit('/protected');
cy.wait('@getData');
});
it('delay response untuk test loading state', () => {
cy.intercept('GET', '/api/slow-data', (req) => {
req.reply({
delay: 3000, // Delay 3 detik
body: { data: 'loaded' },
});
}).as('slowData');
cy.visit('/slow-page');
// Loading indicator should be visible
cy.get('.loading-spinner').should('be.visible');
cy.wait('@slowData');
// Loading should disappear
cy.get('.loading-spinner').should('not.exist');
cy.get('.data-content').should('contain.text', 'loaded');
});
});
7. Custom Commands
Custom Commands memungkinkan Anda membuat perintah reusable yang bisa digunakan di semua test. Ini sangat berguna untuk operasi yang sering dilakukan seperti login, navigasi, dan setup data.
// ═══════════════════════════════════════
// Custom Commands — cypress/support/commands.js
// ═══════════════════════════════════════
// Login via UI
Cypress.Commands.add('loginViaUI', (email, password) => {
cy.session([email, password], () => {
cy.visit('/login');
cy.get('[data-cy="input-email"]').type(email);
cy.get('[data-cy="input-password"]').type(password);
cy.get('[data-cy="btn-login"]').click();
cy.url().should('include', '/dashboard');
});
});
// Login via API (lebih cepat)
Cypress.Commands.add('loginViaAPI', (email, password) => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password },
}).then((response) => {
expect(response.status).to.eq(200);
window.localStorage.setItem('auth-token', response.body.token);
window.localStorage.setItem('user', JSON.stringify(response.body.user));
});
});
// Select element by data-cy attribute (convenience)
Cypress.Commands.add('getByCy', (selector) => {
return cy.get(`[data-cy="${selector}"]`);
});
// Select element by test-id
Cypress.Commands.add('getByTestId', (selector) => {
return cy.get(`[data-testid="${selector}"]`);
});
// Upload gambar
Cypress.Commands.add('uploadImage', (selector, fileName) => {
cy.get(selector).selectFile(`cypress/fixtures/images/${fileName}`);
});
// Seed database via API
Cypress.Commands.add('seedDatabase', () => {
cy.task('seedDatabase');
});
// Verifikasi toast notification
Cypress.Commands.add('expectToast', (message, type = 'success') => {
cy.get(`.toast.toast-${type}`)
.should('be.visible')
.and('contain.text', message);
// Tunggu toast hilang
cy.get(`.toast.toast-${type}`, { timeout: 5000 })
.should('not.exist');
});
// Tunggu halaman selesai loading
Cypress.Commands.add('waitForPage', () => {
cy.get('[data-cy="page-loading"]', { timeout: 10000 }).should('not.exist');
cy.get('body').should('be.visible');
});
Menggunakan Custom Commands
import users from '../fixtures/users.json';
describe('Dashboard', () => {
beforeEach(() => {
// Login via API (cepat, tidak perlu UI)
cy.loginViaAPI(users.admin.email, users.admin.password);
cy.visit('/dashboard');
cy.waitForPage();
});
it('menampilkan dashboard untuk admin', () => {
cy.getByCy('judul-dashboard')
.should('contain.text', 'Dashboard');
cy.getByCy('stat-total-users')
.should('be.visible');
cy.getByCy('stat-total-orders')
.should('be.visible');
});
it('admin bisa mengakses halaman manajemen', () => {
cy.getByCy('nav-admin').click();
cy.url().should('include', '/admin');
cy.getByCy('admin-panel').should('be.visible');
});
it('menampilkan notifikasi', () => {
cy.getByCy('btn-notifications').click();
cy.getByCy('notification-panel')
.should('be.visible')
.within(() => {
cy.get('.notification-item').should('have.length.gte', 1);
});
});
});
8. Testing Patterns
Page Object Pattern
// Page Object: Login Page
class LoginPage {
visit() {
cy.visit('/login');
return this;
}
fillEmail(email) {
cy.getByCy('input-email').clear().type(email);
return this;
}
fillPassword(password) {
cy.getByCy('input-password').clear().type(password);
return this;
}
submit() {
cy.getByCy('btn-login').click();
return this;
}
login(email, password) {
this.fillEmail(email);
this.fillPassword(password);
this.submit();
return this;
}
expectError(message) {
cy.getByCy('error-message')
.should('be.visible')
.and('contain.text', message);
return this;
}
expectRedirectTo(path) {
cy.url().should('include', path);
return this;
}
}
export default new LoginPage();
// Penggunaan di test:
// import loginPage from '../pages/LoginPage';
// loginPage.visit().login('budi@example.com', 'Pass123!').expectRedirectTo('/dashboard');
9. CI/CD Integration
Cypress sangat mudah diintegrasikan dengan CI/CD platforms. Berikut adalah konfigurasi untuk beberapa platform populer:
GitHub Actions
name: Cypress E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Run Cypress tests
uses: cypress-io/github-action@v6
with:
start: npm start
wait-on: 'http://localhost:3000'
wait-on-timeout: 120
browser: chrome
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload screenshots (on failure)
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-videos
path: cypress/videos
Running Tests di Berbagai Mode
# Buka Cypress Test Runner (GUI) npx cypress open # Jalankan semua test headless npx cypress run # Jalankan test file tertentu npx cypress run --spec "cypress/e2e/login.cy.js" # Jalankan dengan browser tertentu npx cypress run --browser chrome npx cypress run --browser firefox npx cypress run --browser edge # Jalankan dengan tag/heading tertentu (grep) npx cypress run --env grep="login" # Record ke Cypress Dashboard npx cypress run --record --key your-record-key # Parallel execution (butuh Cypress Dashboard) npx cypress run --record --parallel --group "e2e-chrome" --browser chrome # Jalankan dengan environment variables npx cypress run --env API_URL=http://staging.example.com
- Gunakan
cy.session()untuk meng-cache login antar test - Aktifkan
retriesdi CI untuk mengurangi flaky tests - Gunakan
video: trueuntuk debug test yang gagal - Upload artifacts (screenshots/videos) ke CI pipeline
- Pisahkan test suite: smoke tests (fast) vs full regression (thorough)
- Gunakan environment variables untuk konfigurasi staging/production
10. Quiz Pemahaman
Uji pemahaman Anda tentang Cypress E2E Testing:
1. Mengapa Cypress lebih cepat dari Selenium?
2. Apa fungsi cy.intercept()?
3. Mengapa disarankan menggunakan [data-cy] sebagai selector?
4. Apa keuntungan cy.fixture() dalam testing?
5. Untuk apa cy.session() digunakan?