Post

Pengantar Cypress โ€” End-to-End Testing untuk Aplikasi Web Modern

Pengantar Cypress โ€” End-to-End Testing untuk Aplikasi Web Modern

๐ŸŒ 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

LevelToolsFokus Testing
โœ… Unit TestingJest, JUnit, MochaTesting fungsi individual
โœ… Integration TestingPostman, SupertestTesting interaksi antar modul
โœ… End-to-End Testing (E2E)Cypress, Playwright, SeleniumTesting 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:

AspekCypressSelenium
ArsitekturBerjalan di browserButuh WebDriver eksternal
SetupSimple, npm installPerlu download driver manual
DebuggingTime travel, screenshot otomatisManual debugging
WaitingAutomatic retry & waitManual explicit wait
SpeedLebih cepatLebih lambat
Learning CurveLebih mudahLebih kompleks
Multi-browserChrome, Firefox, EdgeSemua browser
Multi-tabTerbatasFull support

Keunggulan Cypress:

KelebihanPenjelasan
๐Ÿ”ฅ Real-time previewBisa lihat browser bergerak otomatis
๐ŸŽฎ Mudah digunakanSintaks mirip bahasa manusia โ†’ cocok mahasiswa & QA
๐Ÿš€ Cepat & stabilTidak butuh WebDriver seperti Selenium
๐Ÿ’ฌ Error log jelasCypress menjelaskan error secara otomatis
๐Ÿค Integrasi CI/CDBisa dijalankan di GitHub Actions, Jenkins, dsb
๐ŸŽฅ Video recordingOtomatis merekam test execution
๐Ÿ“ธ Auto screenshotScreenshot otomatis saat test fail
โฑ๏ธ Time travelBisa 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:

FolderFungsi
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:

CommandFungsiContoh
cy.visit()Buka URLcy.visit('https://example.com')
cy.get()Cari elemency.get('#username')
cy.contains()Cari textcy.contains('Login')
cy.click()Klik elemency.get('button').click()
cy.type()Ketik textcy.get('input').type('Hello')
cy.should()Assertioncy.get('h1').should('be.visible')
cy.wait()Tunggu (ms)cy.wait(1000)
cy.reload()Refresh halamancy.reload()
cy.url()Dapatkan URLcy.url().should('include', '/dashboard')
cy.title()Dapatkan titlecy.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!


This post is licensed under CC BY 4.0 by the author.