๐ 1. Apa Itu Cypress?
Cypress adalah framework End-to-End (E2E) Testing modern yang digunakan untuk menguji aplikasi web langsung di browser seperti pengguna asli. Cypress berjalan real-time, sehingga tester dapat melihat bagaimana browser berinteraksi secara otomatis tanpa klik manual.
๐ฏ Fokus Cypress:
โMeniru perilaku user sungguhan untuk memastikan alur aplikasi berjalan mulus.โ
Karakteristik Utama Cypress:
- โ
All-in-one testing framework (tidak perlu tools tambahan)
- โ
Berjalan langsung di browser yang sama dengan aplikasi
- โ
Automatic waiting (tidak perlu manual wait)
- โ
Real-time reloading saat test berubah
- โ
Time travel debugging
- โ
Screenshot dan video otomatis untuk failed tests
๐ 2. Cypress dalam Piramida Testing
| Level | Tools | Fokus Testing |
|---|
| โ
Unit Testing | Jest, JUnit, Mocha | Testing fungsi individual |
| โ
Integration Testing | Postman, Supertest | Testing interaksi antar modul |
| โ
End-to-End Testing (E2E) | Cypress, Playwright, Selenium | Testing alur user lengkap |
๐ก E2E Testing memastikan aplikasi bekerja dari perspektif user, bukan hanya kode.
โก 3. Kenapa Cypress Banyak Dipakai QA Modern?
Perbandingan Cypress vs Selenium:
| Aspek | Cypress | Selenium |
|---|
| Arsitektur | Berjalan di browser | Butuh WebDriver eksternal |
| Setup | Simple, npm install | Perlu download driver manual |
| Debugging | Time travel, screenshot otomatis | Manual debugging |
| Waiting | Automatic retry & wait | Manual explicit wait |
| Speed | Lebih cepat | Lebih lambat |
| Learning Curve | Lebih mudah | Lebih kompleks |
| Multi-browser | Chrome, Firefox, Edge | Semua browser |
| Multi-tab | Terbatas | Full support |
Keunggulan Cypress:
| Kelebihan | Penjelasan |
|---|
| ๐ฅ Real-time preview | Bisa lihat browser bergerak otomatis |
| ๐ฎ Mudah digunakan | Sintaks mirip bahasa manusia โ cocok mahasiswa & QA |
| ๐ Cepat & stabil | Tidak butuh WebDriver seperti Selenium |
| ๐ฌ Error log jelas | Cypress menjelaskan error secara otomatis |
| ๐ค Integrasi CI/CD | Bisa dijalankan di GitHub Actions, Jenkins, dsb |
| ๐ฅ Video recording | Otomatis merekam test execution |
| ๐ธ Auto screenshot | Screenshot otomatis saat test fail |
| โฑ๏ธ Time travel | Bisa lihat state aplikasi di setiap step |
โ 4. Instalasi Cypress
Prerequisites:
- Node.js versi 14+ (download di https://nodejs.org)
- NPM atau Yarn package manager
Step 1: Inisialisasi Project
1
2
3
4
5
6
| # Buat folder project baru
mkdir cypress-testing
cd cypress-testing
# Inisialisasi npm
npm init -y
|
Step 2: Install Cypress
1
2
| # Install Cypress sebagai dev dependency
npm install cypress --save-dev
|
Step 3: Buka Cypress
1
2
| # Buka Cypress Test Runner
npx cypress open
|
Pertama kali dibuka, Cypress akan otomatis membuat struktur folder:
1
2
3
4
5
| cypress/
โโโ e2e/ # Test files
โโโ fixtures/ # Test data (JSON)
โโโ support/ # Custom commands & helpers
โโโ videos/ # Test recordings
|
Step 4: Verifikasi Instalasi
Jalankan example test yang sudah disediakan Cypress untuk memastikan instalasi berhasil.
๐ 5. Struktur Folder Cypress
1
2
3
4
5
6
7
8
9
10
11
12
13
| project-root/
โโโ cypress/
โ โโโ e2e/ # Folder test files
โ โ โโโ example.cy.js
โ โโโ fixtures/ # Test data (JSON, CSV)
โ โ โโโ users.json
โ โโโ support/ # Custom commands & config
โ โ โโโ commands.js # Custom Cypress commands
โ โ โโโ e2e.js # Runs before every test
โ โโโ downloads/ # Downloaded files
โโโ cypress.config.js # Cypress configuration
โโโ package.json
โโโ node_modules/
|
Penjelasan Folder:
| Folder | Fungsi |
|---|
| e2e/ | Tempat menyimpan semua test files |
| fixtures/ | Data statis untuk testing (JSON/CSV) |
| support/ | Custom commands dan helper functions |
| downloads/ | File yang didownload saat test |
| screenshots/ | Screenshot otomatis saat test fail |
| videos/ | Video recording test execution |
๐งช 6. Test Pertama dengan Cypress
File: cypress/e2e/first-test.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| describe('Test Pertama Saya', () => {
it('Harus bisa membuka Google', () => {
// Buka website
cy.visit('https://www.google.com')
// Validasi title
cy.title().should('include', 'Google')
})
it('Harus bisa melakukan pencarian', () => {
cy.visit('https://www.google.com')
// Cari search box dan ketik
cy.get('textarea[name="q"]').type('Cypress Testing{enter}')
// Tunggu hasil muncul
cy.url().should('include', 'search')
// Validasi hasil pencarian ada
cy.get('#search').should('be.visible')
})
})
|
Menjalankan Test:
1
2
3
4
5
6
7
8
9
10
11
| # Buka Test Runner (GUI mode)
npx cypress open
# Jalankan test di terminal (Headless mode)
npx cypress run
# Jalankan test spesifik
npx cypress run --spec "cypress/e2e/first-test.cy.js"
# Jalankan di browser tertentu
npx cypress run --browser chrome
|
๐ฏ 7. Syntax Dasar Cypress
Struktur Test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| describe('Nama Test Suite', () => {
before(() => {
// Jalankan sekali sebelum semua test
cy.log('Setup sebelum semua test')
})
beforeEach(() => {
// Jalankan sebelum setiap test
cy.visit('https://example.com')
})
afterEach(() => {
// Jalankan setelah setiap test
cy.log('Test selesai')
})
after(() => {
// Jalankan sekali setelah semua test
cy.log('Cleanup setelah semua test')
})
it('Test case pertama', () => {
// Test code here
})
it('Test case kedua', () => {
// Test code here
})
})
|
Commands Paling Sering Digunakan:
| Command | Fungsi | Contoh |
|---|
cy.visit() | Buka URL | cy.visit('https://example.com') |
cy.get() | Cari elemen | cy.get('#username') |
cy.contains() | Cari text | cy.contains('Login') |
cy.click() | Klik elemen | cy.get('button').click() |
cy.type() | Ketik text | cy.get('input').type('Hello') |
cy.should() | Assertion | cy.get('h1').should('be.visible') |
cy.wait() | Tunggu (ms) | cy.wait(1000) |
cy.reload() | Refresh halaman | cy.reload() |
cy.url() | Dapatkan URL | cy.url().should('include', '/dashboard') |
cy.title() | Dapatkan title | cy.title().should('eq', 'Home') |
๐ 8. Selector (Menemukan Elemen)
Berbagai Cara Mencari Elemen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // By CSS Selector
cy.get('#username') // ID
cy.get('.btn-primary') // Class
cy.get('button') // Tag
cy.get('[type="submit"]') // Attribute
cy.get('form > button') // Parent-child
// By Text Content
cy.contains('Login') // Text exact
cy.contains('button', 'Login') // Button dengan text Login
// By Data Attribute (Best Practice)
cy.get('[data-cy="login-btn"]') // Custom data attribute
// Multiple Selectors
cy.get('input[type="email"]').first()
cy.get('li').last()
cy.get('li').eq(2) // Index ke-2
|
Best Practice Selector:
1
2
3
4
5
| <!-- โ Tidak disarankan (mudah berubah) -->
<button class="btn btn-primary submit-btn">Login</button>
<!-- โ
Disarankan (stable) -->
<button data-cy="login-button">Login</button>
|
1
2
| // Gunakan data-cy attribute
cy.get('[data-cy="login-button"]').click()
|
๐ก 9. Interaksi dengan Elemen
Input Text:
1
2
3
4
5
6
7
8
9
10
11
12
| // Ketik text
cy.get('#username').type('user@example.com')
// Ketik dengan delay
cy.get('#password').type('password123', { delay: 100 })
// Clear text
cy.get('#search').clear()
// Type dengan special keys
cy.get('#search').type('Cypress{enter}')
cy.get('input').type('{selectall}{backspace}')
|
Click & Interaction:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Click biasa
cy.get('button').click()
// Double click
cy.get('.item').dblclick()
// Right click
cy.get('.item').rightclick()
// Click dengan options
cy.get('button').click({ force: true }) // Force click even if covered
cy.get('button').click({ multiple: true }) // Click all matching elements
// Hover (trigger mouseover)
cy.get('.menu').trigger('mouseover')
|
Checkbox & Radio:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Check checkbox
cy.get('[type="checkbox"]').check()
// Check specific checkbox
cy.get('#agree-terms').check()
// Uncheck
cy.get('[type="checkbox"]').uncheck()
// Check radio button
cy.get('[type="radio"][value="male"]').check()
// Check multiple checkboxes
cy.get('[type="checkbox"]').check(['option1', 'option2'])
|
Select Dropdown:
1
2
3
4
5
6
7
8
9
10
11
| // Select by text
cy.get('select').select('Indonesia')
// Select by value
cy.get('select').select('id')
// Select by index
cy.get('select').select(0)
// Select multiple
cy.get('select[multiple]').select(['option1', 'option2'])
|
File Upload:
1
2
3
4
5
6
7
8
9
10
11
| // Upload file
cy.get('input[type="file"]').selectFile('cypress/fixtures/image.png')
// Upload multiple files
cy.get('input[type="file"]').selectFile([
'cypress/fixtures/file1.pdf',
'cypress/fixtures/file2.pdf'
])
// Drag and drop file
cy.get('input[type="file"]').selectFile('image.png', { action: 'drag-drop' })
|
โ
10. Assertions (Validasi)
Should Assertions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // Visibility
cy.get('.alert').should('be.visible')
cy.get('.hidden').should('not.be.visible')
cy.get('.element').should('exist')
cy.get('.deleted').should('not.exist')
// Text content
cy.get('h1').should('contain', 'Welcome')
cy.get('h1').should('have.text', 'Welcome')
cy.get('.error').should('include.text', 'invalid')
// Value
cy.get('input').should('have.value', 'test@example.com')
cy.get('input').should('be.empty')
// Attributes
cy.get('button').should('have.attr', 'disabled')
cy.get('a').should('have.attr', 'href', '/dashboard')
cy.get('.active').should('have.class', 'selected')
// State
cy.get('button').should('be.enabled')
cy.get('button').should('be.disabled')
cy.get('[type="checkbox"]').should('be.checked')
cy.get('[type="checkbox"]').should('not.be.checked')
// Length
cy.get('li').should('have.length', 5)
cy.get('li').should('have.length.greaterThan', 3)
// URL & Title
cy.url().should('include', '/dashboard')
cy.url().should('eq', 'https://example.com/dashboard')
cy.title().should('contain', 'Dashboard')
|
Multiple Assertions:
1
2
3
4
5
| cy.get('button')
.should('be.visible')
.and('be.enabled')
.and('have.text', 'Submit')
.and('have.class', 'btn-primary')
|
Custom Assertions:
1
2
3
4
5
| cy.get('.list').should(($list) => {
expect($list).to.have.length(3)
expect($list.eq(0)).to.contain('Item 1')
expect($list.eq(1)).to.contain('Item 2')
})
|
๐ 11. Contoh Test Case Lengkap: Login Flow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| describe('Login Feature', () => {
beforeEach(() => {
// Buka halaman login sebelum setiap test
cy.visit('https://example.com/login')
})
it('Harus menampilkan form login', () => {
// Validasi elemen form ada
cy.get('[data-cy="username"]').should('be.visible')
cy.get('[data-cy="password"]').should('be.visible')
cy.get('[data-cy="login-btn"]').should('be.visible')
cy.contains('Login').should('be.visible')
})
it('Harus berhasil login dengan credentials valid', () => {
// Input username
cy.get('[data-cy="username"]').type('testuser@example.com')
// Input password
cy.get('[data-cy="password"]').type('Test@123')
// Klik login button
cy.get('[data-cy="login-btn"]').click()
// Validasi redirect ke dashboard
cy.url().should('include', '/dashboard')
// Validasi welcome message
cy.contains('Welcome back').should('be.visible')
// Validasi user profile muncul
cy.get('[data-cy="user-profile"]').should('be.visible')
})
it('Harus menampilkan error dengan credentials invalid', () => {
// Input credentials salah
cy.get('[data-cy="username"]').type('wrong@example.com')
cy.get('[data-cy="password"]').type('wrongpassword')
cy.get('[data-cy="login-btn"]').click()
// Validasi error message muncul
cy.get('[data-cy="error-message"]')
.should('be.visible')
.and('contain', 'Invalid credentials')
// Validasi masih di halaman login
cy.url().should('include', '/login')
})
it('Harus menampilkan error jika field kosong', () => {
// Klik login tanpa isi form
cy.get('[data-cy="login-btn"]').click()
// Validasi error untuk username
cy.get('[data-cy="username-error"]')
.should('be.visible')
.and('contain', 'Username is required')
// Validasi error untuk password
cy.get('[data-cy="password-error"]')
.should('be.visible')
.and('contain', 'Password is required')
})
it('Harus bisa toggle password visibility', () => {
// Input password
cy.get('[data-cy="password"]').type('Test@123')
// Password harus type password (hidden)
cy.get('[data-cy="password"]').should('have.attr', 'type', 'password')
// Klik toggle button
cy.get('[data-cy="toggle-password"]').click()
// Password harus jadi type text (visible)
cy.get('[data-cy="password"]').should('have.attr', 'type', 'text')
})
})
|
๐ง 12. Custom Commands
Buat reusable commands di cypress/support/commands.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Login command
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login')
cy.get('[data-cy="username"]').type(email)
cy.get('[data-cy="password"]').type(password)
cy.get('[data-cy="login-btn"]').click()
})
// Logout command
Cypress.Commands.add('logout', () => {
cy.get('[data-cy="user-menu"]').click()
cy.get('[data-cy="logout-btn"]').click()
})
// Fill form command
Cypress.Commands.add('fillForm', (formData) => {
Object.keys(formData).forEach(key => {
cy.get(`[data-cy="${key}"]`).type(formData[key])
})
})
|
Gunakan di test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| describe('Dashboard Test', () => {
beforeEach(() => {
// Gunakan custom login command
cy.login('user@example.com', 'Test@123')
})
it('Harus menampilkan dashboard setelah login', () => {
cy.url().should('include', '/dashboard')
cy.contains('Dashboard').should('be.visible')
})
afterEach(() => {
// Gunakan custom logout command
cy.logout()
})
})
|
๐ฆ 13. Fixtures (Test Data)
File: cypress/fixtures/users.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| {
"validUser": {
"email": "testuser@example.com",
"password": "Test@123"
},
"invalidUser": {
"email": "wrong@example.com",
"password": "wrongpass"
},
"adminUser": {
"email": "admin@example.com",
"password": "Admin@123"
}
}
|
Gunakan di Test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| describe('Login with Fixtures', () => {
it('Login dengan data dari fixture', () => {
// Load fixture data
cy.fixture('users').then((users) => {
cy.visit('/login')
cy.get('[data-cy="username"]').type(users.validUser.email)
cy.get('[data-cy="password"]').type(users.validUser.password)
cy.get('[data-cy="login-btn"]').click()
cy.url().should('include', '/dashboard')
})
})
})
|
๐ 14. API Testing dengan Cypress
Cypress juga bisa test API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| describe('API Testing', () => {
it('GET - Harus return list users', () => {
cy.request('GET', 'https://jsonplaceholder.typicode.com/users')
.then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.length(10)
expect(response.body[0]).to.have.property('name')
})
})
it('POST - Harus create user baru', () => {
cy.request({
method: 'POST',
url: 'https://jsonplaceholder.typicode.com/users',
body: {
name: 'John Doe',
email: 'john@example.com'
}
}).then((response) => {
expect(response.status).to.eq(201)
expect(response.body).to.have.property('id')
expect(response.body.name).to.eq('John Doe')
})
})
it('Intercept API call', () => {
// Intercept dan mock response
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
]
}).as('getUsers')
cy.visit('/users')
// Wait for intercepted request
cy.wait('@getUsers')
// Validate mocked data displayed
cy.contains('User 1').should('be.visible')
})
})
|
โ
Kesimpulan
- Cypress adalah framework E2E testing modern yang mudah digunakan
- Real-time preview membuat debugging lebih mudah
- Automatic waiting menghilangkan masalah flaky tests
- Custom commands membuat test lebih maintainable
- CI/CD integration membuat testing bagian dari development workflow
- Best practices membuat test suite lebih reliable dan efficient
๐ก Next Step: Praktikkan dengan project real dan integrasikan dengan CI/CD pipeline!