Bắt đầu với Cypress cho Kiểm thử API và Giao diện Người dùng (UI)

Trong hành trình trở thành một Kỹ sư QA xuất sắc, việc nắm vững các công cụ tự động hóa là vô cùng quan trọng. Nếu bạn đã theo dõi Lộ trình học Kỹ sư QA (Tester) 2025 của chúng ta, bạn sẽ thấy tự động hóa là một bước tiến tự nhiên sau khi đã hiểu sâu về Đảm bảo Chất lượng, phát triển tư duy kiểm thử, các loại kiểm thử khác nhau như Black Box, White Box, Gray Box hay Kiểm thử Chức năng và Phi Chức năng, và thậm chí cả kiểm thử thủ công. Hôm nay, chúng ta sẽ khám phá Cypress, một công cụ mạnh mẽ và thân thiện với nhà phát triển, đặc biệt phù hợp cho việc kiểm thử các ứng dụng web hiện đại ở cả tầng Giao diện Người dùng (UI) và API.

Cypress Là Gì? Tại Sao Lại Chọn Cypress?

Cypress là một công cụ kiểm thử tự động mã nguồn mở được xây dựng cho web. Điểm khác biệt lớn nhất của Cypress so với các framework truyền thống như Selenium là kiến trúc của nó. Thay vì chạy bên ngoài trình duyệt và gửi lệnh từ xa, Cypress chạy *trong* cùng một vòng lặp chạy ứng dụng của bạn. Điều này mang lại nhiều lợi thế:

  • Tốc độ: Thực thi lệnh nhanh chóng, không có độ trễ mạng.
  • Độ tin cậy: Tự động chờ các phần tử DOM và các yêu cầu mạng, giảm thiểu lỗi flaky tests (kiểm thử không ổn định).
  • Debug dễ dàng: Giao diện người dùng (Test Runner) trực quan cung cấp ảnh chụp nhanh (snapshot) của ứng dụng ở mỗi bước kiểm thử, log chi tiết các lệnh và trạng thái.
  • Tất cả trong một: Bao gồm framework test, assertion library (Chai, Sinon, jQuery), mocking/stubbing (Sinon), và khả năng xử lý các yêu cầu mạng ngay lập tức.
  • Thân thiện với Developer/QA: Dựa trên JavaScript, ngôn ngữ phổ biến trong phát triển web. Dễ cài đặt và cấu hình.

Trong khi bạn có thể đã tìm hiểu về tự động hóa backend bằng các công cụ như Postman hay Rest-Assured (Tự động hóa Kiểm thử Backend với Postman, Newman và REST Assured), Cypress nổi bật ở khả năng tích hợp kiểm thử API ngay trong luồng kiểm thử giao diện người dùng, hoặc sử dụng API để thiết lập dữ liệu test hiệu quả. Điều này rất hữu ích trong môi trường Agile (Vai trò của Kỹ sư QA trong các Phương pháp Phát triển Agile), nơi tốc độ phản hồi và khả năng kiểm thử end-to-end nhanh chóng là then chốt.

Bắt Đầu: Cài Đặt và Cấu Hình Cơ Bản

Để bắt đầu với Cypress, bạn cần đảm bảo máy tính của mình đã cài đặt Node.js (phiên bản 10 trở lên).

Mở terminal hoặc command prompt trong thư mục gốc của dự án web mà bạn muốn kiểm thử (hoặc tạo một thư mục mới nếu bạn chỉ muốn thử nghiệm). Sau đó, chạy lệnh sau để cài đặt Cypress như một dependency dành cho phát triển:

npm install cypress --save-dev

Hoặc nếu bạn sử dụng Yarn:

yarn add cypress --dev

Sau khi cài đặt hoàn tất, bạn có thể mở Cypress Test Runner lần đầu tiên bằng lệnh:

npx cypress open

Lệnh này sẽ thực hiện một số việc:

  1. Tạo cấu trúc thư mục mặc định của Cypress (thường là một thư mục `cypress` ở gốc dự án).
  2. Mở giao diện Cypress Test Runner.

Trong Test Runner, bạn có thể chọn loại kiểm thử muốn chạy (E2E testing là phổ biến nhất cho UI và API). Cypress sẽ tự động phát hiện các file test trong thư mục `cypress/e2e` (hoặc `cypress/integration` trên các phiên bản cũ hơn).

Cấu trúc thư mục `cypress` bao gồm:

  • `e2e/`: Chứa các file test của bạn (`.cy.js`, `.cy.ts`).
  • `fixtures/`: Nơi lưu trữ dữ liệu test tĩnh (ví dụ: file JSON).
  • `support/`: Chứa các file hỗ trợ như custom commands hoặc cấu hình môi trường.
  • `videos/` và `screenshots/`: Nơi lưu video và ảnh chụp màn hình của các lần chạy test (tự động ghi lại khi chạy từ CLI).

Viết Trường Hợp Kiểm Thử Giao diện Người dùng (UI) Đầu Tiên

Kiểm thử UI với Cypress rất trực quan. Bạn sẽ tương tác với ứng dụng của mình giống như một người dùng thực sự.

Tạo một file mới trong thư mục `cypress/e2e`, ví dụ: `login.cy.js`.

Mỗi file test thường bắt đầu với một khối `describe` để nhóm các kiểm thử liên quan và các khối `it` cho từng trường hợp kiểm thử cụ thể.

describe('Bộ kiểm thử Đăng nhập', () => {
  it('Nên hiển thị thông báo lỗi khi nhập sai thông tin', () => {
    // Truy cập trang đăng nhập
    cy.visit('/login'); // Giả sử ứng dụng đang chạy trên localhost:port

    // Tìm các phần tử và nhập thông tin sai
    cy.get('input[name="username"]').type('sai_ten_nguoi_dung');
    cy.get('input[name="password"]').type('mat_khau_sai');

    // Nhấn nút đăng nhập
    cy.get('button[type="submit"]').click();

    // Kiểm tra xem thông báo lỗi có xuất hiện không
    cy.get('.error-message')
      .should('be.visible') // Đảm bảo phần tử hiển thị
      .and('contain', 'Thông tin đăng nhập không hợp lệ'); // Kiểm tra nội dung
  });

  it('Nên đăng nhập thành công với thông tin đúng', () => {
    cy.visit('/login');

    // Lấy thông tin đăng nhập từ fixture (ví dụ: cypress/fixtures/users.json)
    cy.fixture('users').then((users) => {
      const validUser = users.valid; // Giả sử có một user hợp lệ trong users.json

      cy.get('input[name="username"]').type(validUser.username);
      cy.get('input[name="password"]').type(validUser.password);
    });

    cy.get('button[type="submit"]').click();

    // Kiểm tra xem người dùng đã được chuyển hướng đến trang dashboard chưa
    cy.url().should('include', '/dashboard');

    // Kiểm tra một phần tử nào đó trên trang dashboard để xác nhận đăng nhập thành công
    cy.get('.welcome-message').should('contain', 'Chào mừng');
  });
});

Trong ví dụ trên:

  • `cy.visit(‘/login’)`: Lệnh để truy cập một URL. URL này được nối vào `baseUrl` mà bạn có thể cấu hình trong file `cypress.config.js`.
  • `cy.get(‘selector’)`: Lệnh để tìm kiếm một phần tử trên trang dựa trên CSS selector. Cypress khuyến khích sử dụng các selector không dễ thay đổi như `data-testid` thay vì class hoặc ID động.
  • `.type(‘text’)`: Lệnh để nhập văn bản vào trường input.
  • `.click()`: Lệnh để click vào một phần tử.
  • `.should(‘condition’, ‘value’)`: Lệnh để thực hiện các assertion (khẳng định). Cypress sử dụng thư viện Chai, cho phép bạn kiểm tra nhiều điều kiện khác nhau về trạng thái, nội dung, thuộc tính của phần tử.
  • `cy.url().should(‘include’, ‘/dashboard’)`: Kiểm tra URL hiện tại sau khi hành động.
  • `cy.fixture(‘users’).then(…)`: Cách tốt để quản lý dữ liệu test. File `cypress/fixtures/users.json` có thể trông như sau:

{
  "valid": {
    "username": "tester",
    "password": "password123"
  },
  "invalid": {
    "username": "wronguser",
    "password": "wrongpassword"
  }
}

Khi bạn lưu file `login.cy.js` và Cypress Test Runner đang chạy, nó sẽ tự động phát hiện file mới và bạn có thể click vào tên file đó để chạy kiểm thử. Bạn sẽ thấy ứng dụng của mình được load trong cửa sổ bên phải và các lệnh Cypress được thực thi từng bước ở bảng bên trái. Click vào từng bước lệnh sẽ hiển thị ảnh chụp nhanh của ứng dụng tại thời điểm đó, giúp bạn dễ dàng debug.

Việc viết các trường hợp kiểm thử UI này giúp bạn kiểm tra các luồng người dùng cuối (end-to-end flows), đảm bảo các chức năng quan trọng hoạt động như mong đợi. Đây là sự tiếp nối tự nhiên từ việc viết các trường hợp kiểm thử thủ công, giờ đây được tự động hóa để chạy nhanh hơn và lặp lại được.

Kiểm thử API với Cypress

Ngoài kiểm thử UI, Cypress cũng rất mạnh mẽ trong việc kiểm thử API bằng lệnh `cy.request`. Điểm đặc biệt là `cy.request` *không* hiển thị trên giao diện người dùng hoặc tương tác với trình duyệt. Nó gửi yêu cầu HTTP trực tiếp từ backend của Cypress (Node.js environment) đến máy chủ của ứng dụng bạn đang kiểm thử. Điều này làm cho kiểm thử API với Cypress rất nhanh.

Lợi ích của `cy.request`:

  • Kiểm tra các endpoint backend độc lập.
  • Sử dụng để thiết lập dữ liệu test hoặc trạng thái ban đầu cho các kiểm thử UI (ví dụ: tạo người dùng, xóa dữ liệu cũ).
  • Xác minh các yêu cầu API được thực hiện trong quá trình tương tác UI (sử dụng `cy.intercept`).

Tạo một file mới, ví dụ: `api_tests.cy.js`.

describe('Bộ kiểm thử API', () => {
  const apiUrl = 'http://localhost:3000/api'; // Thay thế bằng URL API của bạn

  it('Nên lấy danh sách người dùng thành công (GET)', () => {
    cy.request('GET', `${apiUrl}/users`)
      .then((response) => {
        // Khẳng định mã trạng thái (status code) là 200 OK
        expect(response.status).to.eq(200);
        // Khẳng định phản hồi là một mảng
        expect(response.body).to.be.an('array');
        // Khẳng định mảng không rỗng (nếu có dữ liệu)
        expect(response.body.length).to.be.greaterThan(0);
        // Khẳng định đối tượng đầu tiên trong mảng có thuộc tính 'id'
        expect(response.body[0]).to.have.property('id');
      });
  });

  it('Nên tạo người dùng mới thành công (POST)', () => {
    const newUser = {
      name: 'Test User',
      email: `testuser_${Date.now()}@example.com`, // Sử dụng timestamp để đảm bảo unique
      password: 'securepassword'
    };

    cy.request('POST', `${apiUrl}/users`, newUser)
      .then((response) => {
        // Khẳng định mã trạng thái là 201 Created
        expect(response.status).to.eq(201);
        // Khẳng định phản hồi có chứa thông tin người dùng đã tạo
        expect(response.body).to.have.property('id');
        expect(response.body).to.include(newUser); // Kiểm tra các thuộc tính đã gửi lên
      });
  });

  it('Nên cập nhật người dùng hiện có thành công (PUT)', () => {
      // Bước 1: Tạo một người dùng trước để có ID để cập nhật
      const userToUpdate = {
          name: 'User to Update',
          email: `update_${Date.now()}@example.com`,
          password: 'initialpassword'
      };
      cy.request('POST', `${apiUrl}/users`, userToUpdate)
          .then((createResponse) => {
              const userId = createResponse.body.id;
              const updatedData = { name: 'Updated User Name' };

              // Bước 2: Cập nhật người dùng
              cy.request('PUT', `${apiUrl}/users/${userId}`, updatedData)
                  .then((updateResponse) => {
                      expect(updateResponse.status).to.eq(200);
                      expect(updateResponse.body.name).to.eq('Updated User Name');
                      expect(updateResponse.body.email).to.eq(userToUpdate.email); // Email không thay đổi
                  });
          });
  });

   it('Nên xóa người dùng thành công (DELETE)', () => {
      // Bước 1: Tạo một người dùng để xóa
      const userToDelete = {
          name: 'User to Delete',
          email: `delete_${Date.now()}@example.com`,
          password: 'deletepassword'
      };
      cy.request('POST', `${apiUrl}/users`, userToDelete)
          .then((createResponse) => {
              const userId = createResponse.body.id;

              // Bước 2: Xóa người dùng
              cy.request('DELETE', `${apiUrl}/users/${userId}`)
                  .then((deleteResponse) => {
                      expect(deleteResponse.status).to.eq(200); // Hoặc 204 No Content tùy API
                  });
          });
  });

});

Trong ví dụ trên, chúng ta sử dụng `cy.request(method, url, body)` để gửi các yêu cầu HTTP (GET, POST, PUT, DELETE). Phương thức `.then()` cho phép chúng ta truy cập vào đối tượng phản hồi (`response`), bao gồm `status`, `body`, và `headers`. Chúng ta sử dụng `expect().to.eq()`, `expect().to.be.an()`, `expect().to.have.property()`, v.v., từ thư viện Chai để kiểm tra các thuộc tính của phản hồi API.

Kết hợp `cy.request` với kiểm thử UI là một kỹ thuật rất mạnh. Ví dụ, thay vì điền form đăng nhập phức tạp trong kiểm thử UI mỗi lần, bạn có thể sử dụng `cy.request` để gọi API đăng nhập, lấy token xác thực hoặc cookie, và sau đó sử dụng chúng để truy cập trực tiếp các trang đã đăng nhập trong kiểm thử UI. Điều này giúp kiểm thử UI tập trung vào giao diện và luồng tương tác sau khi đăng nhập, trong khi quá trình đăng nhập được xử lý nhanh chóng qua API.

describe('Kiểm thử Giao diện sau Đăng nhập (sử dụng API để đăng nhập)', () => {
  it('Nên hiển thị trang dashboard sau khi đăng nhập qua API', () => {
    cy.fixture('users').then((users) => {
      const validUser = users.valid;

      // Sử dụng cy.request để gửi yêu cầu đăng nhập API
      cy.request('POST', 'http://localhost:3000/api/login', {
        username: validUser.username,
        password: validUser.password
      }).then((response) => {
        // Khẳng định đăng nhập thành công
        expect(response.status).to.eq(200);
        // Lấy token hoặc thông tin session từ phản hồi
        const authToken = response.body.token; // Giả sử API trả về token

        // Sử dụng token hoặc thông tin session để truy cập trang dashboard
        // Có nhiều cách tùy thuộc vào cách ứng dụng xử lý xác thực (ví dụ: lưu vào localStorage, cookie)
        cy.visit('/dashboard', {
           onBeforeLoad (win) {
             // Ví dụ: lưu token vào localStorage trước khi load trang
             win.localStorage.setItem('authToken', authToken);
           }
        });

        // Tiếp tục kiểm thử UI trên trang dashboard
        cy.get('.user-profile-name').should('contain', validUser.username);
        cy.get('.dashboard-widget').should('have.length.greaterThan', 0);
      });
    });
  });
});

Kỹ thuật này giúp giảm thời gian chạy kiểm thử UI và làm cho chúng đáng tin cậy hơn, vì bạn không phụ thuộc vào việc giao diện form đăng nhập có hoạt động đúng trong mỗi lần chạy kiểm thử dashboard hay không.

So Sánh Kiểm thử UI và API với Cypress

Dưới đây là bảng tóm tắt sự khác biệt và điểm chung khi sử dụng Cypress cho hai loại kiểm thử này:

Aspect Kiểm thử Giao diện Người dùng (UI) Kiểm thử API (với cy.request)
Mục tiêu chính Xác minh trải nghiệm người dùng, luồng công việc, tương tác với các phần tử trên trang. Đảm bảo giao diện hoạt động như mong đợi. Kiểm tra logic nghiệp vụ backend, tính đúng đắn của dữ liệu trả về, tích hợp giữa các dịch vụ (nếu API gọi các dịch vụ khác).
Cách thực hiện Cypress chạy *trong* trình duyệt, mô phỏng hành động của người dùng (click, type, scroll, …). Tương tác với DOM. Cypress gửi các yêu cầu HTTP trực tiếp từ môi trường Node.js đến server backend. Không tương tác với trình duyệt hoặc DOM.
Tốc độ thực thi Chậm hơn, vì phải chờ trình duyệt render, các hiệu ứng animation, và tương tác người dùng. Nhanh hơn đáng kể, bỏ qua bước render giao diện và chờ đợi tương tác UI.
Độ tin cậy Có thể bị ảnh hưởng bởi sự thay đổi nhỏ trong giao diện hoặc thời gian load trang. Cypress có cơ chế chờ tự động tốt, nhưng vẫn có thể gặp vấn đề. Ổn định hơn, ít phụ thuộc vào giao diện người dùng. Chủ yếu phụ thuộc vào tính ổn định và hiệu năng của API backend.
Commands chính cy.visit(), cy.get(), .type(), .click(), .should(), cy.wait(), cy.intercept() (để theo dõi/stub API từ UI). cy.request(), các assertion trên đối tượng response (response.status, response.body, …).
Debug Sử dụng Cypress Test Runner trực quan, ảnh chụp nhanh, video, log lệnh, console trình duyệt. Chủ yếu kiểm tra log của lệnh cy.request trong Test Runner hoặc console, xem thông tin phản hồi API.
Ứng dụng Kiểm thử end-to-end, kiểm thử acceptance. Đảm bảo toàn bộ hệ thống hoạt động liền mạch từ góc độ người dùng. Kiểm thử service level, kiểm thử tích hợp backend. Dùng để thiết lập dữ liệu test nhanh chóng cho UI.
Yêu cầu Ứng dụng web cần chạy và có giao diện để tương tác. API endpoint cần hoạt động và có thể truy cập được.

Như bạn có thể thấy, cả hai loại kiểm thử đều có vai trò riêng biệt và bổ trợ cho nhau. Cypress cung cấp một nền tảng thống nhất để thực hiện cả hai, giúp giảm bớt sự phức tạp khi phải làm việc với nhiều công cụ khác nhau.

Lời Khuyên và Các Bước Tiếp Theo

* Bắt đầu từ những trường hợp đơn giản: Đừng cố gắng tự động hóa mọi thứ cùng lúc. Bắt đầu với các luồng quan trọng và ổn định nhất của ứng dụng.
* Sử dụng Selector hiệu quả: Tránh sử dụng các CSS class hoặc ID dễ thay đổi. Ưu tiên `data-testid` hoặc các thuộc tính ổn định khác mà developer có thể thêm vào cho mục đích kiểm thử.
* Tổ chức code: Sử dụng custom commands (`cypress/support/commands.js`) cho các thao tác lặp đi lặp lại (ví dụ: đăng nhập). Tách test logic khỏi dữ liệu test (sử dụng fixtures).
* Tìm hiểu về `cy.intercept()`: Lệnh này cực kỳ hữu ích để theo dõi, chờ đợi, hoặc thậm chí mock (giả lập) các yêu cầu mạng từ giao diện người dùng. Điều này giúp bạn kiểm soát phản hồi API trong khi kiểm thử UI và giảm sự phụ thuộc vào backend thật.
* Khám phá Cypress Test Runner: Dành thời gian làm quen với giao diện của Test Runner. Chức năng Time-Traveling Debugging (quay ngược thời gian xem ảnh chụp nhanh ở từng bước) là một công cụ vô giá để hiểu điều gì đang xảy ra.
* Kết hợp API để thiết lập test: Áp dụng kỹ thuật sử dụng `cy.request` để tạo dữ liệu test hoặc đặt ứng dụng vào trạng thái cần thiết trước khi bắt đầu tương tác UI. Điều này giúp test UI nhanh hơn và đáng tin cậy hơn.

Việc học và sử dụng Cypress cho cả kiểm thử API và UI sẽ là một bước tiến lớn trong lộ trình của một Kỹ sư QA hiện đại. Nó không chỉ trang bị cho bạn kỹ năng tự động hóa mạnh mẽ mà còn giúp bạn hiểu rõ hơn về cách các ứng dụng web hoạt động ở cả hai tầng frontend và backend.

Tiếp theo trong series “QA Engineer (Tester) Roadmap”, chúng ta sẽ đi sâu hơn vào các kỹ thuật tự động hóa nâng cao, hoặc có thể khám phá cách tích hợp kiểm thử tự động vào quy trình CI/CD để đạt được phản hồi nhanh chóng về chất lượng phần mềm. Hãy tiếp tục theo dõi!

Chỉ mục