0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Những Điều Cần Nhớ Khi Kiểm Tra Ứng Dụng React

Đăng vào 7 tháng trước

• 8 phút đọc

Giới thiệu

Nhiều nhóm phát triển mà tôi đã làm việc cùng đều gặp phải vấn đề tương tự: Họ có viết kiểm tra, nhưng không ai tin tưởng vào chúng. Báo cáo độ bao phủ (coverage report) luôn xanh, nhưng quy trình CI lại chậm chạp và kéo dài, và rồi những lỗi vẫn lọt vào sản xuất. Lý do là vì các nhà phát triển thường bỏ qua việc chạy thử nghiệm vì họ biết một nửa trong số đó không đáng tin cậy hoặc không có ý nghĩa.

Đó là một nguy cơ thực sự: Khi một đội ngừng tin tưởng vào các bài kiểm tra, toàn bộ bộ kiểm tra trở thành gánh nặng, chỉ là mã thừa để duy trì mà không tạo ra sự tự tin. Mọi người đều học được bài học: Một bộ kiểm tra không đáng tin còn tệ hơn cả việc không có kiểm tra nào.

Vậy, làm thế nào để chúng ta tránh được điều đó? Điều gì khiến các bài kiểm tra trở nên đáng tin cậy thay vì chỉ tồn tại?

Dưới đây là một số bài học mà tôi đã học được qua những trải nghiệm thực tế.

Độ Bao Phủ Không Bằng Chất Lượng

Tôi đã từng làm việc trên những dự án mà việc đạt được “90% độ bao phủ” được coi là một cột mốc quan trọng. Bảng điều khiển trở nên xanh, quản lý mỉm cười, và... chúng tôi vẫn phát hành các tính năng có lỗi.

Tại sao lại như vậy? Bởi vì độ bao phủ chỉ đo lường các dòng mã đã được thực thi, chứ không phải liệu bài kiểm tra có chứng minh ứng dụng hoạt động hay không.

Ví dụ:

javascript Copy
it("renders the login form", () => {
  const { container } = render(<LoginForm onSubmit={() => {}} />);
  expect(container.querySelector("input[name='email']")).toBeVisible();
});

Bài kiểm tra này thực thi hầu hết mọi dòng trong thành phần, vì vậy độ bao phủ tăng cao. Nhưng nếu ai đó xóa logic gửi đi vào ngày mai, nó vẫn sẽ qua. Bộ kiểm tra trông rất tốt trên giấy - nhưng tính năng thì bị lỗi.

Điều bạn thực sự cần là các bài kiểm tra dựa trên ý định.

javascript Copy
it("submits user login", () => {
  const handleSubmit = vi.fn();
  render(<LoginForm onSubmit={handleSubmit} />);

  fireEvent.change(screen.getByPlaceholderText(/email/i), { target: { value: "user@example.com" } });
  fireEvent.change(screen.getByPlaceholderText(/password/i), { target: { value: "password" } });
  fireEvent.click(screen.getByRole("button", { name: /submit/i }));

  expect(handleSubmit).toHaveBeenCalledWith({ email: "user@example.com", password: "password" });
});

Bài kiểm tra này không chỉ chạy mã. Nó bảo vệ ý định của người dùng: nếu tôi nhập thông tin và nhấn gửi, ứng dụng có hoạt động đúng không?

Độ bao phủ là một chỉ số hữu ích, nhưng không phải là mục tiêu. Sự tin tưởng đến từ việc kiểm tra ý nghĩa, không phải chỉ là các dòng mã.

Cách Chọn Phần Tử Quan Trọng

Một thủ phạm giấu mặt khác cướp đi sự tin tưởng (và hiệu suất) là cách bạn chọn phần tử.

Các bài kiểm tra sớm thường dựa vào các bộ chọn dễ bị hỏng:

javascript Copy
expect(screen.querySelector(".btn-primary")).toBeVisible();

Vấn đề ở đây? Thay đổi lớp CSS → bài kiểm tra thất bại, mặc dù giao diện vẫn hoạt động.

Hoặc sử dụng quá nhiều thuộc tính data-testid ở khắp nơi.

javascript Copy
expect(screen.getByTestId("submit-btn")).toBeVisible();

Nó có hoạt động - có, nhưng tôi thường thấy mọi người mang theo những tệp hằng số lớn vào trong bundle của họ với các giá trị cần thiết cho test ids.

Đó là lý do tại sao Thư viện Kiểm Tra React khuyên dùng các truy vấn ngữ nghĩa:

  • getByRole → “đây là một nút”
  • getByLabelText → “input được gán nhãn Email”
  • getByText → “người dùng có thể đọc được văn bản này”

Nó buộc bạn phải kiểm tra giao diện theo cách mà người dùng tương tác với nó. Và một lần nữa - đó là điều xây dựng lòng tin.

Những Gì (và Khi Nào) Cần Giả Lập

Đây là nơi nhiều nhóm mất niềm tin vào các bài kiểm tra của họ. Giả lập (mocking) là một công cụ mạnh mẽ, nhưng nó cũng đi kèm với trách nhiệm lớn.

Giả lập quá ít → các bài kiểm tra sẽ truy cập mạng hoặc cơ sở dữ liệu → chậm, không ổn định, không đáng tin cậy.
Giả lập quá nhiều → bạn không còn kiểm tra thực tế, chỉ đang kiểm tra các giả lập của riêng bạn.

Các quy tắc tôi tuân theo:

  • Hệ thống và mạng bên ngoài → luôn giả lập (các cuộc gọi mạng, DB, hệ thống tệp, v.v.)
  • Tiện ích thuần túy → không giả lập (chỉ cần sử dụng trực tiếp, chúng ảnh hưởng trực tiếp đến đầu ra và kỳ vọng của bạn)
  • Các mô-đun / thành phần nội bộ → ở đây bạn cần phải cẩn thận. Bạn cần giữ sự cân bằng để tránh tạo ra khoảng cách giữa những gì bạn cần kiểm tra và cách mà ứng dụng của bạn thực sự hoạt động.

Một lần chúng tôi gặp phải một vấn đề trong một dự án khi toàn bộ mô-đun authService bị giả lập:

javascript Copy
vi.mock("../authService", () => {
    return {
        login: vi.fn().mockResolvedValue({ token: "fake" })
    };
});

Bộ kiểm tra luôn thành công, nhưng một ngày nọ chúng tôi gặp vấn đề nghiêm trọng trong sản xuất liên quan đến quản lý ủy quyền. Không bài kiểm tra nào phát hiện ra điều đó - vì bài kiểm tra không sử dụng authService thực tế, nó chỉ kiểm tra giả lập của nó, và khi đầu ra của dịch vụ thay đổi, mọi thứ vẫn xanh.

Giải pháp? Đừng giả lập dịch vụ bản thân. Giả lập nguồn bên ngoài mà dịch vụ phụ thuộc vào. Ví dụ, chặn các cuộc gọi fetch với MSW. Bạn vẫn tránh được các cuộc gọi mạng thực tế, nhưng vẫn giữ nguyên logic dịch vụ của bạn.

Đó là chìa khóa: giả lập ở ranh giới hệ thống, không ở bên trong lõi ứng dụng của bạn.

Kiểm Tra Đơn Vị So Với Kiểm Tra Tích Hợp (Chúng Ta Có Vẫn Tin Vào Kim Tự Tháp?)

Bạn còn nhớ “kim tự tháp kiểm tra” không? Rất nhiều kiểm tra đơn vị ở dưới cùng, ít kiểm tra tích hợp ở giữa, và một vài kiểm tra e2e ở trên cùng.

Trong các dự án React, kim tự tháp đó thường bị sụp đổ. Chúng tôi kết thúc với hàng ngàn bài kiểm tra đơn vị nhỏ cho các hook và nút... nhưng chúng thực sự không bắt được lỗi. Chúng không nắm bắt được toàn bộ bức tranh của những gì đang xảy ra, chúng sống trong thế giới riêng biệt của chúng.

Hầu hết các lỗi mà tôi đã thấy xảy ra giữa các đơn vị:

  • Dữ liệu không được truyền chính xác.
  • Kiểm tra không hoạt động.
  • Phản hồi API bị xử lý không đúng cách.
  • Promises không được xử lý.

Đó là lý do tại sao nhiều nhà phát triển hiện nay nghiêng về Trophy Kiểm Tra:

  • Kiểm tra đơn vị cho logic thuần túy
  • Kiểm tra tích hợp cho các luồng (người dùng điền mẫu → API được gọi → thông báo thành công hiển thị)
  • Kiểm tra e2e cho các con đường quan trọng

Tôi sẽ không nói rằng kim tự tháp đã chết - nhưng trong thực tế, một tư duy tích hợp trước đã mang lại cho tôi nhiều niềm tin hơn một đại dương các bài kiểm tra đơn vị riêng biệt.

Kiểm Tra Snapshot: Một Mạng Lưới An Toàn Sai Lầm

Snapshots cảm thấy như ma thuật lúc đầu. Một dòng mã, độ bao phủ ngay lập tức:

javascript Copy
expect(container).toMatchSnapshot();

Trên thực tế:

  • Snapshots thường xuyên bị hỏng trên các thay đổi nhỏ trong markup.
  • Sự khác biệt rất lớn và không thể đọc được.
  • Các nhà phát triển bắt đầu nhấn “cập nhật snapshot” mà không nhìn.

Tại thời điểm đó, các bài kiểm tra không bảo vệ bất cứ điều gì. Chúng chỉ là công việc tốn thời gian.

Sự thật rất đơn giản: snapshots không chứng minh hành vi. Chúng không cho bạn biết liệu ứng dụng có thực sự hoạt động cho người dùng hay không.

Bạn luôn nhận được nhiều giá trị hơn từ việc viết các khẳng định rõ ràng:

javascript Copy
expect(screen.getByRole("heading", { name: /welcome/i })).toBeVisible();

Định Tính So Với Sự Không Ổn Định

Tôi đã chứng kiến các bộ kiểm tra sụp đổ do sự không ổn định. Khi các nhà phát triển ngừng tin tưởng vào bộ kiểm tra, họ ngừng chạy nó. Và từ đó, nó chết.

Thủ phạm luôn giống nhau:

  • Bộ đếm thời gian thực (setTimeout, setInterval)
  • Các giá trị ngẫu nhiên (UUIDs, Math.random)
  • Điều kiện đua bất đồng bộ
  • Sử dụng quá nhiều waitFor với thời gian chờ tùy ý

Phương pháp chữa trị là định tính:

  • Đóng băng thời gian (vi.setSystemTime).
  • Giả lập ngẫu nhiên (vi.mock("uuid")).
  • Sử dụng bộ đếm thời gian giả.

Không phải là làm cho các bài kiểm tra “kém thực tế.” Mà là làm cho chúng đáng tin cậy. Nếu một bài kiểm tra đôi khi thành công và đôi khi thất bại, nó còn tệ hơn cả việc không có bài kiểm tra nào cả.

Kiểm Tra Không Ổn Định: Phát Hiện và Loại Bỏ

Khi những bài kiểm tra không ổn định xuất hiện, lòng tin sẽ ra đi. Đó là lý do tại sao việc tìm và sửa chữa chúng nhanh chóng là rất quan trọng.

Các công cụ hỗ trợ:

  • Jest → --detectOpenHandles, --listTestsByDuration
  • Vitest → --slowTestThreshold

Nhưng phòng ngừa còn tốt hơn:

  • Tránh để lại trạng thái giữa các bài kiểm tra.
  • Không dựa vào thời gian chờ tùy ý.
  • Luôn chờ cho các điều kiện rõ ràng (như findByRole).

Khi tôi thấy một bài kiểm tra không ổn định trong CI, tôi không phớt lờ nó. Tôi hoặc là cách ly nó hoặc sửa nó ngay lập tức. Bởi vì càng lâu một bài kiểm tra không ổn định sống trong chính, thì bộ kiểm tra của bạn càng nhanh chóng mất đi độ tin cậy.

Kết luận

Lịch sử của kiểm tra frontend là một lịch sử của sự tự tin sai lầm:

  • Số liệu độ bao phủ không có nghĩa là an toàn.
  • Snapshots không có nghĩa là ổn định.
  • Mocks không có nghĩa là thực tế.

Bài học? Các bài kiểm tra không phải là về con số. Chúng là về niềm tin.

Một bài kiểm tra tốt trả lời hai câu hỏi:

  • Nếu nó thất bại, tôi có biết điều gì quan trọng đã hỏng không?
  • Nếu nó thành công, tôi có tin rằng tính năng hoạt động không?

Nếu câu trả lời là “không,” bài kiểm tra chỉ là tiếng ồn.

Đó là điều tôi nhắc nhở bản thân mỗi khi tôi viết các bài kiểm tra. Bởi vì cuối cùng, tôi không muốn nhiều bài kiểm tra hơn. Tôi muốn một bộ kiểm tra mà đội ngũ của tôi thực sự tin tưởng.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào