Bài 33.3: End-to-end testing với Cypress

Bài 33.3: End-to-end testing với Cypress
Chào mừng các bạn đã quay trở lại với series về Lập trình Web Front-end! Sau khi đã xây dựng nên những giao diện đẹp mắt và tương tác mượt mà bằng HTML, CSS, JavaScript, TypeScript, React hay NextJs, một câu hỏi quan trọng đặt ra là: Làm thế nào để đảm bảo toàn bộ ứng dụng của chúng ta hoạt động trơn tru như mong đợi khi người dùng thực sự sử dụng nó?
Đây chính là lúc End-to-end testing (E2E testing) phát huy sức mạnh của mình. E2E testing mô phỏng lại toàn bộ hành trình của người dùng trên ứng dụng của bạn, từ lúc mở trình duyệt, tương tác với các phần tử, điều hướng giữa các trang, cho đến khi hoàn thành một tác vụ cụ thể (ví dụ: đăng nhập, đặt hàng, gửi form). Nó kiểm tra toàn bộ hệ thống từ giao diện người dùng đến back-end, cơ sở dữ liệu và các dịch vụ bên ngoài, đảm bảo mọi thứ "nói chuyện" được với nhau một cách chính xác.
Trong thế giới E2E testing cho ứng dụng web, Cypress đã nhanh chóng nổi lên như một cái tên nổi bật và được yêu thích. Tại sao vậy? Bởi vì Cypress được xây dựng đặc biệt dành cho các lập trình viên front-end và các kỹ sư QA hiện đại. Nó mang lại trải nghiệm tuyệt vời, tốc độ nhanh chóng và độ tin cậy cao hơn nhiều so với các công cụ truyền thống.
Tại Sao Chọn Cypress Cho End-to-end Testing?
Cypress không chỉ là một công cụ chạy script tự động trên trình duyệt. Nó là một khung làm việc testing hoàn chỉnh với nhiều tính năng được tích hợp sẵn, giúp việc viết và chạy test trở nên dễ dàng và hiệu quả hơn bao giờ hết. Dưới đây là một số lý do khiến Cypress trở nên hấp dẫn:
- Developer Experience Tuyệt Vời: Cypress cung cấp một giao diện trực quan để xem test chạy như thế nào trong thời gian thực. Bạn có thể thấy từng bước test được thực thi, các lệnh Cypress chạy và ứng dụng phản ứng ra sao.
- Automatic Waiting: Một trong những "nỗi đau" lớn nhất khi làm E2E testing là phải xử lý các yếu tố bất đồng bộ (asynchronous operations) và chờ các phần tử load xong. Cypress tự động chờ các phần tử DOM hiển thị, các animation kết thúc, và các AJAX request hoàn thành trước khi tiếp tục bước tiếp theo. Điều này giúp giảm đáng thiểu tình trạng test bị flaky (lúc pass lúc fail không rõ lý do).
- Time Travel: Cypress chụp ảnh nhanh (snapshot) của ứng dụng ở mỗi bước thực thi lệnh. Bạn có thể di chuột qua các lệnh trong giao diện test runner và xem ứng dụng của bạn trông như thế nào tại thời điểm đó. Điều này cực kỳ hữu ích cho việc debug.
- Dễ Cài Đặt và Sử Dụng: Chỉ cần một vài dòng lệnh npm/yarn là bạn đã có thể bắt đầu. Cypress được viết hoàn toàn bằng JavaScript và chạy trực tiếp trong cùng một "run loop" với ứng dụng của bạn, không giống như các công cụ dựa trên Selenium giao tiếp với trình duyệt từ bên ngoài.
- Debug Dễ Dàng: Với công cụ Developer Tools quen thuộc của trình duyệt và các tính năng debug của Cypress, việc tìm và sửa lỗi trong test trở nên nhanh chóng.
- Kiểm Soát Mạng: Cypress cho phép bạn stub (giả lập) hoặc intercept (chặn và sửa đổi) các HTTP requests, giúp bạn kiểm soát dữ liệu mà ứng dụng nhận được và test các kịch bản edge cases dễ dàng hơn.
Cài Đặt và Chạy Cypress
Để bắt đầu với Cypress, bạn cần có Node.js và npm/yarn đã cài đặt. Trong thư mục project của bạn, chạy lệnh sau:
npm install cypress --save-dev
# hoặc với yarn
yarn add cypress --dev
Sau khi cài đặt xong, bạn có thể mở Test Runner của Cypress bằng lệnh:
npx cypress open
Lần đầu tiên chạy lệnh này, Cypress sẽ tự động tạo ra một cấu trúc thư mục mẫu (cypress/
) với các ví dụ test để bạn tham khảo. Giao diện Test Runner sẽ hiển thị, cho phép bạn chọn trình duyệt để chạy test và xem kết quả.
Cấu Trúc Cơ Bản Của Một Bài Test Cypress
Các bài test trong Cypress được viết bằng JavaScript (hoặc TypeScript) và dựa trên cấu trúc của Mocha, một framework testing phổ biến. Dưới đây là cấu trúc cơ bản:
// cypress/e2e/my_first_test.cy.js
// describe: Dùng để nhóm các bài test liên quan lại với nhau.
// Thường đại diện cho một tính năng hoặc một trang trong ứng dụng.
describe('Tính năng Đăng nhập', () => {
// it: Đại diện cho một bài test case cụ thể.
// Tên của it() nên mô tả rõ hành động và kết quả mong đợi.
it('Nên đăng nhập thành công với thông tin hợp lệ', () => {
// Các lệnh Cypress để thực hiện hành động và kiểm tra kết quả
// sẽ được viết ở đây.
});
// Bạn có thể có nhiều test case trong cùng một describe block
it('Nên hiển thị thông báo lỗi với thông tin không hợp lệ', () => {
// Các lệnh test cho trường hợp đăng nhập thất bại
});
});
describe('Tên tính năng', callback)
: Định nghĩa một bộ (suite) các bài test.Tên tính năng
là một chuỗi mô tả, vàcallback
là một hàm chứa các bài test cụ thể bên trong.it('Mô tả test case', callback)
: Định nghĩa một bài test case đơn lẻ.Mô tả test case
giải thích mục đích của test, vàcallback
chứa các lệnh Cypress để thực hiện test.
Các Lệnh Cypress Cơ Bản và Phổ Biến
Cypress cung cấp một bộ lệnh mạnh mẽ để tương tác với ứng dụng của bạn. Hầu hết các lệnh bắt đầu với cy.
và có thể được xâu chuỗi (chaining) lại với nhau để tạo thành các luồng hành động phức tạp.
Dưới đây là một số lệnh cơ bản bạn sẽ dùng rất thường xuyên:
cy.visit(url)
:- Mục đích: Điều hướng trình duyệt đến một URL cụ thể. Đây thường là lệnh đầu tiên trong một bài test.
- Ví dụ:
it('Nên tải trang chủ', () => { cy.visit('http://localhost:3000'); });
- Giải thích: Lệnh này yêu cầu Cypress mở trình duyệt và truy cập vào địa chỉ
http://localhost:3000
.
cy.get(selector)
:- Mục đích: Chọn một hoặc nhiều phần tử DOM dựa trên CSS selector. Đây là lệnh phổ biến nhất để tương tác với các phần tử trên trang.
- Ví dụ:
it('Nên tìm thấy tiêu đề trang', () => { cy.visit('/'); cy.get('h1'); // Chọn phần tử h1 đầu tiên tìm được });
it('Nên tìm thấy tất cả các nút', () => { cy.visit('/'); cy.get('button'); // Chọn TẤT CẢ các phần tử button });
- Giải thích:
cy.get()
trả về một "Subject" của Cypress, đại diện cho phần tử hoặc tập hợp các phần tử đã chọn. Các lệnh tiếp theo thường được xâu chuỗi vào Subject này. Cypress sẽ tự động chờ cho đến khi phần tử xuất hiện trong DOM trước khi trả về.
cy.contains(selector, text)
hoặccy.contains(text)
:- Mục đích: Chọn phần tử dựa trên nội dung văn bản của nó. Rất hữu ích khi selector không ổn định hoặc bạn muốn tìm một nút dựa trên nhãn của nó.
- Ví dụ:
it('Nên tìm thấy nút "Đăng nhập"', () => { cy.visit('/login'); cy.contains('button', 'Đăng nhập'); // Chọn nút có text 'Đăng nhập' // Hoặc nếu chỉ có 1 phần tử duy nhất có text này // cy.contains('Đăng nhập'); });
- Giải thích: Tương tự
cy.get()
, lệnh này cũng trả về một Subject.cy.contains()
thông minh hơn, nó sẽ cố gắng tìm phần tử chứa đoạn text được cung cấp.
.type(text)
:- Mục đích: Gõ văn bản vào một phần tử đầu vào (input, textarea). Phải được xâu chuỗi sau một lệnh chọn phần tử (ví dụ:
cy.get()
hoặccy.contains()
). - Ví dụ:
it('Nên điền thông tin vào form', () => { cy.visit('/contact'); cy.get('#name').type('Nguyen Van A'); cy.get('[data-cy=email-input]').type('a.nguyen@example.com'); // Dùng data-* attribute là practice tốt });
- Giải thích:
.type()
là một "Child command" (lệnh con) hoạt động trên Subject được trả về từ lệnh trước đó (cy.get()
trong trường hợp này). Nó mô phỏng việc gõ phím của người dùng.
- Mục đích: Gõ văn bản vào một phần tử đầu vào (input, textarea). Phải được xâu chuỗi sau một lệnh chọn phần tử (ví dụ:
.click()
:- Mục đích: Nhấp vào một phần tử. Phải được xâu chuỗi sau lệnh chọn phần tử. Cypress sẽ tự động cuộn đến phần tử và đảm bảo nó có thể tương tác được (không bị che phủ) trước khi click.
- Ví dụ:
it('Nên nhấp vào nút gửi', () => { cy.visit('/form'); cy.get('button[type="submit"]').click(); });
- Giải thích: Tương tự
.type()
,.click()
là một Child command thực hiện hành động nhấp chuột.
.should('assertion', 'value')
:- Mục đích: Đây là lệnh quan trọng nhất để kiểm tra xem ứng dụng của bạn có đang ở trạng thái mong đợi hay không. Nó thực hiện các khẳng định (assertions) về trạng thái của phần tử hoặc trang.
Ví dụ về các assertion phổ biến:
it('Nên hiển thị thông báo thành công', () => { cy.visit('/status'); cy.get('.success-message') .should('be.visible') // Kiểm tra phần tử có hiển thị không .and('contain', 'Operation successful'); // Kiểm tra nội dung văn bản }); it('Trường input nên có giá trị mặc định', () => { cy.visit('/settings'); cy.get('#items-per-page') .should('have.value', '10'); // Kiểm tra giá trị của input/select }); it('Nút "Xóa" nên bị vô hiệu hóa lúc đầu', () => { cy.get('.delete-button') .should('be.disabled'); // Kiểm tra trạng thái disabled }); it('Nên có ít nhất 5 mục trong danh sách', () => { cy.get('ul.item-list li') .should('have.length.gte', 5); // Kiểm tra số lượng phần tử });
- Giải thích:
.should()
là một Child command. Nó nhận một chuỗi mô tả kiểu assertion (ví dụ:'be.visible'
,'contain'
,'have.value'
) và đôi khi là một giá trị để so sánh. Cypress sẽ tự động thử lại assertion trong một khoảng thời gian nhất định cho đến khi nó đúng hoặc hết thời gian chờ (timeout), giúp xử lý các tình huống bất đồng bộ. Bạn có thể xâu chuỗi nhiều assertion bằng.and()
.
Ví Dụ Minh Họa: Test Tính Năng Đăng Nhập Đơn Giản
Hãy kết hợp các lệnh trên để viết một bài test E2E đơn giản cho một trang đăng nhập giả định:
// cypress/e2e/login.cy.js
describe('Tính năng Đăng nhập', () => {
// beforeEach: Lệnh này sẽ chạy MỖI LẦN trước khi một bài test case (it)
// trong block describe này được thực thi. Rất hữu ích để thiết lập trạng thái ban đầu
// như truy cập trang web cần test.
beforeEach(() => {
cy.visit('/login'); // Giả sử trang đăng nhập của bạn là /login
});
it('Nên đăng nhập thành công với thông tin hợp lệ', () => {
// 1. Tìm trường username và điền thông tin
cy.get('#username').type('valid_user');
// 2. Tìm trường password và điền thông tin
cy.get('#password').type('correct_password');
// 3. Tìm nút đăng nhập và click vào đó
cy.get('button[type="submit"]').click();
// 4. Kiểm tra kết quả sau khi đăng nhập thành công
// - URL nên chuyển sang trang dashboard
cy.url().should('include', '/dashboard');
// - Nên có một phần tử hiển thị thông báo chào mừng
cy.get('.welcome-message')
.should('be.visible')
.and('contain', 'Chào mừng, valid_user');
});
it('Nên hiển thị thông báo lỗi với thông tin không hợp lệ', () => {
// 1. Điền thông tin đăng nhập SAI
cy.get('#username').type('invalid_user');
cy.get('#password').type('wrong_password');
// 2. Click nút đăng nhập
cy.get('button[type="submit"]').click();
// 3. Kiểm tra thông báo lỗi
// - Nên có một phần tử hiển thị thông báo lỗi
cy.get('.error-message')
.should('be.visible')
.and('contain', 'Tên đăng nhập hoặc mật khẩu không đúng');
// - URL vẫn ở trang đăng nhập (hoặc không phải trang dashboard)
cy.url().should('not.include', '/dashboard');
});
});
- Giải thích:
beforeEach(() => { cy.visit('/login'); });
: Đảm bảo rằng trước mỗi bài test (it
), trình duyệt luôn được đưa về trang/login
. Điều này giúp các test case độc lập với nhau.cy.get('#username').type('valid_user');
: Chọn phần tử input có id làusername
và gõ chuỗi'valid_user'
vào đó.cy.get('button[type="submit"]').click();
: Chọn phần tử button có thuộc tínhtype="submit"
và mô phỏng hành động click.cy.url().should('include', '/dashboard');
: Khẳng định rằng URL hiện tại của trình duyệt chứa chuỗi/dashboard
.cy.get('.welcome-message').should('be.visible').and('contain', '...');
: Chọn phần tử có classwelcome-message
, khẳng định nó hiển thị và chứa đoạn text được cung cấp.
Chạy Test Trong Headless Mode (Dành Cho CI/CD)
Ngoài việc sử dụng giao diện Test Runner (npx cypress open
), bạn có thể chạy test từ dòng lệnh mà không cần mở trình duyệt (headless mode). Điều này rất hữu ích khi tích hợp Cypress vào quy trình Continuous Integration/Continuous Deployment (CI/CD).
npx cypress run
Lệnh này sẽ chạy tất cả các file test trong thư mục cypress/e2e
mặc định trên trình duyệt Electron (headless). Bạn cũng có thể chỉ định trình duyệt khác (--browser chrome
) hoặc chạy một file test cụ thể (--spec cypress/e2e/login.cy.js
). Sau khi chạy xong, Cypress sẽ hiển thị kết quả trong terminal và tạo video + ảnh chụp màn hình (screenshots) cho các test bị fail (mặc định).
Comments