Mở đầu — vì sao bài này quan trọng
Nếu bạn đã theo dõi khóa học tới đây, hẳn bạn đã làm quen với Selenium WebDriver — công cụ tự động hóa UI kinh điển, "ông tổ" của ngành. Selenium mạnh mẽ, đa ngôn ngữ, đa trình duyệt, nhưng đi kèm là sự cồng kềnh: bạn phải tự dựng WebDriver, tự quản lý wait, tự cấu hình driver binary, và không ít lần vò đầu bứt tai vì test "flaky" (chập chờn) do đồng bộ hóa thời gian.
Cypress ra đời năm 2017 với một triết lý hoàn toàn khác. Thay vì đứng bên ngoài điều khiển trình duyệt qua một giao thức trung gian, Cypress chạy ngay bên trong trình duyệt, cùng một vòng lặp sự kiện (event loop) với ứng dụng của bạn. Sự khác biệt kiến trúc này nghe có vẻ nhỏ, nhưng nó thay đổi toàn bộ trải nghiệm viết test: nhanh hơn, ổn định hơn, dễ debug hơn.
Vì sao một kỹ sư QA/SDET ở Việt Nam năm 2026 cần biết Cypress? Vì phần lớn startup và công ty sản phẩm đang tuyển QA đều dùng stack JavaScript/TypeScript cho frontend (React, Vue, Next.js). Khi frontend viết bằng JS, việc test cũng viết bằng JS giúp lập trình viên và tester nói chung một "ngôn ngữ", chia sẻ code, review lẫn nhau. Nhiều tin tuyển dụng QA Automation tại TP.HCM và Hà Nội hiện liệt kê Cypress ngang hàng, thậm chí trên Selenium. Bài học này giúp bạn hiểu Cypress là gì, khác Selenium ra sao, khi nào nên chọn nó, và viết được test đầu tiên một cách tự tin.
Khái niệm cốt lõi
Cypress chạy trong browser — điều đó nghĩa là gì
Selenium hoạt động theo mô hình client-server: code test của bạn (client) gửi lệnh qua giao thức WebDriver tới một trình điều khiển (chromedriver, geckodriver...), trình điều khiển này mới thực thi hành động trên trình duyệt. Mỗi lệnh phải đi qua nhiều lớp mạng nội bộ, tạo độ trễ và điểm gãy.
Cypress lật ngược mô hình đó. Nó tiêm code test trực tiếp vào cùng ngữ cảnh chạy với ứng dụng web. Vì "sống chung nhà" với ứng dụng, Cypress có quyền truy cập trực tiếp vào mọi thứ: DOM, network request, local storage, các đối tượng JavaScript trong bộ nhớ. Đây là lý do Cypress có thể làm những việc Selenium khó làm, như chặn (stub) một API call và trả về dữ liệu giả để test trạng thái lỗi mà không cần backend thật.
Cơ chế retry và automatic waiting
Đây là "vũ khí" lớn nhất của Cypress. Với Selenium, nếu phần tử chưa xuất hiện, lệnh findElement ném lỗi ngay lập tức — bạn phải tự viết WebDriverWait để chờ. Với Cypress, mỗi lệnh truy vấn như cy.get('.button') sẽ tự động thử lại trong một khoảng timeout (mặc định 4 giây) cho tới khi phần tử tồn tại và thỏa điều kiện. Bạn gần như không phải viết wait thủ công. Điều này loại bỏ phần lớn nguyên nhân gây flaky test — vấn đề mà cả một bài riêng (Bài 28) sẽ bàn sâu.
So sánh Cypress và Selenium
| Tiêu chí | Selenium | Cypress |
|---|---|---|
| Kiến trúc | Client–server, ngoài browser | Chạy trong browser |
| Ngôn ngữ | Java, Python, C#, JS, Ruby... | Chỉ JavaScript/TypeScript |
| Chờ đợi (wait) | Thủ công (implicit/explicit) | Tự động retry |
| Đa trình duyệt | Rất rộng (kể cả Safari, IE) | Chrome, Edge, Firefox, Electron, WebKit (thử nghiệm) |
| Đa tab / nhiều domain | Hỗ trợ tốt | Hạn chế (có workaround) |
| Debug | Log ngoài, khó xem lại | Time-travel, snapshot từng bước |
| Network stubbing | Cần proxy ngoài | Tích hợp sẵn (cy.intercept) |
| Cài đặt | Cần driver binary | npm install, xong |
| Chạy song song | Grid tự dựng | Cypress Cloud / tự chia file |
Time-travel debugging
Khi chạy test ở chế độ giao diện (Cypress App), mỗi lệnh được ghi lại kèm một ảnh chụp DOM tại thời điểm đó. Bạn di chuột qua từng bước trong bảng lệnh bên trái, và khung xem bên phải "tua ngược thời gian" để hiển thị chính xác trạng thái trang lúc lệnh chạy. Đây là trải nghiệm debug mà Selenium không có sẵn, tiết kiệm rất nhiều thời gian truy vết lỗi.
Những giới hạn cần biết trước
Cypress không phải "viên đạn bạc". Nó chỉ hỗ trợ JavaScript/TypeScript — nếu team bạn thuần Java, đây là rào cản. Nó chạy trong browser nên khó thao tác đa tab, khó test nhiều domain khác nhau trong một test (do chính sách same-origin, dù các phiên bản gần đây đã có cy.origin). Nó cũng không phải công cụ cho mobile app hay desktop. Hiểu rõ giới hạn giúp bạn chọn đúng chỗ để dùng.
Tình huống thực tế
Ví dụ 1: Startup fintech ở TP.HCM chuyển từ Selenium sang Cypress
Một startup ví điện tử tại Quận 1 (gọi là "PayNhanh") có suite 320 test Selenium viết bằng Java. Vấn đề: tỷ lệ flaky lên tới 12%, mỗi lần chạy CI có khoảng 38 test đỏ ngẫu nhiên, buộc QA phải chạy lại (re-run) thủ công. Nguyên nhân chủ yếu là các Thread.sleep() rải rác và wait không đủ với ứng dụng React tải bất đồng bộ.
Team quyết định thí điểm viết lại 80 test luồng quan trọng nhất (đăng nhập, nạp tiền, chuyển khoản) bằng Cypress. Nhờ cơ chế tự retry, họ xóa toàn bộ sleep. Kết quả sau 6 tuần: tỷ lệ flaky của 80 test này giảm còn 1,5%; thời gian chạy giảm từ 14 phút xuống 6 phút do không còn chờ cứng. Quan trọng hơn, hai lập trình viên frontend bắt đầu tự viết test Cypress cho tính năng mới của mình vì cú pháp JS quen thuộc.
Bài học: Cypress tỏa sáng với ứng dụng SPA (single-page application) tải bất đồng bộ, nơi flaky test là nỗi đau lớn nhất. Đừng viết lại toàn bộ ngay — hãy thí điểm ở nhóm test giá trị cao để chứng minh giá trị.
Ví dụ 2: Test trạng thái lỗi mà không cần backend — công ty thương mại điện tử
Một sàn TMĐT ở Hà Nội cần test giao diện khi API giỏ hàng trả về lỗi 500 (mất kết nối). Với Selenium, họ phải nhờ team backend dựng một môi trường giả lập trả lỗi — mất công điều phối giữa các team, và không phải lúc nào cũng làm được.
Với Cypress, QA dùng cy.intercept('POST', '/api/cart', { statusCode: 500 }) để chặn ngay request và ép nó trả về lỗi. Test kiểm tra xem giao diện có hiện thông báo "Không thể thêm vào giỏ, vui lòng thử lại" và nút retry hay không. Toàn bộ làm trong 15 dòng code, không đụng tới backend.
Bài học: Khả năng stub network tích hợp sẵn của Cypress cho phép test các "đường biên" (edge case) như lỗi mạng, timeout, phản hồi rỗng — những kịch bản cực khó dựng bằng công cụ ngoài browser. Đây là lợi thế mà nhiều team đánh giá còn cao hơn cả tốc độ.
Ví dụ 3: Khi Cypress không phải lựa chọn đúng
Một công ty gia công phần mềm (outsourcing) ở Đà Nẵng nhận dự án test cho khách Nhật, yêu cầu bắt buộc phải cover cả Safari trên macOS và luồng đăng nhập qua cổng SSO của bên thứ ba (chuyển hướng sang một domain khác rồi quay lại). Đội thử dùng Cypress nhưng vấp hai bức tường: Cypress không hỗ trợ Safari đầy đủ, và việc nhảy qua nhiều domain khiến test phức tạp, dễ gãy.
Cuối cùng họ giữ Cypress cho phần lớn luồng nội bộ (chiếm 90% test), nhưng dùng Playwright (Bài 13) cho các kịch bản đa domain và đa trình duyệt. Đây là kiến trúc "công cụ đúng cho việc đúng".
Bài học: Chọn công cụ theo ràng buộc thực tế của dự án, không theo trào lưu. Nếu bắt buộc Safari hoặc luồng đa domain phức tạp, hãy cân nhắc kỹ trước khi cam kết Cypress.
Hướng dẫn từng bước
Hãy cùng dựng và chạy test Cypress đầu tiên. Giả định bạn đã cài Node.js.
Bước 1 — Khởi tạo dự án và cài Cypress
mkdir cypress-demo && cd cypress-demo
npm init -y
npm install cypress --save-dev
Chỉ một lệnh npm install, không cần tải driver binary như Selenium.
Bước 2 — Mở Cypress App lần đầu
npx cypress open
Lần đầu chạy, Cypress tạo sẵn thư mục cypress/ với cấu trúc chuẩn (e2e/, fixtures/, support/) và file cypress.config.js. Chọn "E2E Testing" để bắt đầu.
Bước 3 — Viết test đầu tiên
Tạo file cypress/e2e/login.cy.js:
describe('Luồng đăng nhập', () => {
beforeEach(() => {
cy.visit('https://example.com/login')
}) it('đăng nhập thành công với thông tin hợp lệ', () => {
cy.get('[data-cy=email]').type('user@vietnamcos.com')
cy.get('[data-cy=password]').type('MatKhau123')
cy.get('[data-cy=submit]').click()
// Cypress tự chờ URL đổi và phần tử xuất hiện
cy.url().should('include', '/dashboard')
cy.contains('Xin chào').should('be.visible')
})
})
Chú ý: không có một dòng wait nào. Cypress tự retry cho tới khi điều kiện should được thỏa. Cũng chú ý cách dùng thuộc tính data-cy làm selector — đây là best practice để selector không phụ thuộc vào class CSS hay text dễ thay đổi.
Bước 4 — Thêm test cho trạng thái lỗi với network stub
it('hiện lỗi khi server trả về 401', () => {
cy.intercept('POST', '/api/login', {
statusCode: 401,
body: { message: 'Sai email hoặc mật khẩu' }
}).as('loginFail') cy.get('[data-cy=email]').type('sai@email.com')
cy.get('[data-cy=password]').type('sai')
cy.get('[data-cy=submit]').click()
cy.wait('@loginFail')
cy.contains('Sai email hoặc mật khẩu').should('be.visible')
})
Bước 5 — Chạy ở chế độ headless cho CI
npx cypress run
Lệnh này chạy toàn bộ test không cần giao diện, tự quay video và chụp ảnh khi lỗi — hoàn hảo để tích hợp vào pipeline CI/CD (sẽ bàn ở các bài sau về GitHub Actions và Jenkins).
Lỗi thường gặp & mẹo
1. Cố dùng async/await như Promise thường. Lệnh Cypress trông giống Promise nhưng thực chất là hàng đợi lệnh (command queue) được xếp lịch, không chạy tức thì. Đừng viết const el = await cy.get(...). Hãy dùng .then() khi cần lấy giá trị: cy.get('.total').then($el => { ... }).
2. Lạm dụng cy.wait(3000) với thời gian cứng. Đây là thói quen mang từ Selenium sang và nó phá hỏng ưu điểm của Cypress. Thay vì chờ cứng, hãy chờ đúng thứ bạn cần: cy.wait('@apiAlias') để chờ network, hoặc để cơ chế retry của should tự lo.
3. Dùng selector dựa trên CSS class hoặc text UI. Class thay đổi khi refactor style, text thay đổi khi đổi ngôn ngữ. Hãy thống nhất với dev đặt thuộc tính data-cy (hoặc data-testid) cho các phần tử cần test. Đây là quy ước giúp test bền vững nhất.
4. Quên rằng Cypress reset trạng thái giữa các test. Mỗi it chạy sạch, không giữ đăng nhập từ test trước. Dùng beforeEach để thiết lập lại, hoặc dùng cy.session() để cache phiên đăng nhập, tránh đăng nhập lại tốn thời gian.
5. Kỳ vọng test đa tab. Cypress không mở tab mới được. Nếu ứng dụng mở link ra tab mới, mẹo là kiểm tra thuộc tính target của thẻ <a> rồi cy.visit thẳng tới URL đó, thay vì thực sự mở tab.
Mẹo vàng: Bật cy.intercept để "nhìn" mọi request ứng dụng gửi đi. Kể cả khi không stub, nó giúp bạn hiểu app gọi API nào, giúp viết test chính xác và debug nhanh hơn nhiều.
Bài tập thực hành
- Cài đặt và chạy: Khởi tạo một dự án Cypress mới, mở Cypress App, và chạy các test mẫu có sẵn. Quan sát tính năng time-travel — di chuột qua từng lệnh và xem DOM thay đổi.
- Viết test luồng thật: Chọn một trang có form đăng ký công khai (hoặc trang demo như
https://the-internet.herokuapp.com/login). Viết test kiểm tra: đăng nhập sai báo lỗi, đăng nhập đúng chuyển trang. Dùngdata-*selector nếu có, và tuyệt đối không dùngcy.waitvới số cứng.
- Thử stub network: Với một trang gọi API công khai bất kỳ, dùng
cy.interceptđể ép API trả về lỗi 500, rồi kiểm chứng giao diện xử lý lỗi thế nào. Nếu app không xử lý lỗi — đó chính là một bug đáng báo cáo.
- So sánh: Viết ra một bảng ngắn liệt kê 3 điểm bạn thấy Cypress dễ hơn Selenium và 2 điểm Cypress hạn chế hơn, dựa trên trải nghiệm thực tế của chính bạn trong bài tập này.
Tóm tắt
Cypress là công cụ tự động hóa UI hiện đại, chạy ngay trong trình duyệt thay vì điều khiển từ bên ngoài như Selenium. Kiến trúc "sống chung nhà" với ứng dụng mang lại ba lợi thế lớn: tự động retry loại bỏ phần lớn flaky test, network stubbing tích hợp sẵn giúp test edge case dễ dàng, và time-travel debugging giúp truy vết lỗi nhanh. Đổi lại, Cypress giới hạn ở JavaScript/TypeScript, hỗ trợ đa trình duyệt hẹp hơn, và khó xử lý đa tab/đa domain.
Nguyên tắc chọn công cụ: Cypress là lựa chọn tuyệt vời cho các SPA (React, Vue, Next.js), cho team dùng stack JS, và cho những dự án mà flaky test đang là nỗi đau. Nhưng nếu bạn cần Safari, cần luồng đa domain phức tạp, hay team thuần Java, hãy cân nhắc Selenium hoặc Playwright. Ở bài tiếp theo, chúng ta sẽ tìm hiểu Playwright — một "hậu bối" trẻ hơn, giải quyết chính những giới hạn mà Cypress còn vướng. Hãy nắm chắc Cypress trước, vì tư duy tự-retry và network-stubbing bạn học ở đây sẽ theo bạn suốt sự nghiệp automation.