Vitest: Framework Kiểm Thử Nhanh Chóng của Vite (React Roadmap)

Chào mừng các bạn quay trở lại với chuỗi bài viết React Roadmap! Trong những bài viết trước, chúng ta đã cùng nhau khám phá những khái niệm cốt lõi của React, từ React là gì, cách làm việc với Functional và Class Components, JSX, Props và State, xử lý Conditional Rendering, Lists và Keys, xử lý sự kiện, đến những kiến thức nâng cao hơn về Hooks, Custom Hooks, Memoization với useCallback, useMemo, và quản lý State phức tạp hơn với useReducer, useContext, và các thư viện như Redux Toolkit, Recoil/Zustand, hay MobX. Chúng ta cũng đã tìm hiểu về cách Routing với React Router, Data Fetching, và xử lý Form.

Một khía cạnh cực kỳ quan trọng trong phát triển phần mềm hiện đại, đặc biệt là với các ứng dụng frontend ngày càng phức tạp, chính là Kiểm Thử (Testing). Chúng ta đã dành thời gian tìm hiểu về việc viết Unit Test cơ bản với Jest, cách kiểm thử hành vi người dùng với React Testing Library (RTL), và cả Kiểm Thử End-to-End (E2E) với Cypress.

Jest đã và đang là một công cụ kiểm thử phổ biến và mạnh mẽ, đặc biệt trong cộng đồng React. Tuy nhiên, với sự trỗi dậy mạnh mẽ của các công cụ build và development server thế hệ mới như Vite, một framework kiểm thử được sinh ra để tận dụng tối đa ưu điểm tốc độ của Vite đã xuất hiện và nhanh chóng nhận được sự quan tâm: Vitest.

Trong bài viết này, chúng ta sẽ cùng tìm hiểu Vitest là gì, tại sao nó lại được coi là một lựa chọn hấp dẫn, đặc biệt là trong kỷ nguyên Vite, và làm thế nào để bắt đầu sử dụng Vitest để kiểm thử các ứng dụng React của mình.

Vitest Là Gì và Tại Sao Nó Ra Đời?

Vitest là một framework kiểm thử được xây dựng bởi chính đội ngũ phát triển Vite. Mục tiêu chính của nó là cung cấp một trải nghiệm kiểm thử nhanh chóng, nhẹ nhàng và liền mạch, tận dụng kiến trúc native ESM (ECMAScript Modules) của Vite.

Sự ra đời của Vitest là phản ứng tự nhiên trước sự phổ biến của Vite. Vite đã cách mạng hóa trải nghiệm phát triển frontend bằng cách loại bỏ quá trình bundling nặng nề trong môi trường dev, sử dụng ESM native của trình duyệt để tải và cập nhật code siêu nhanh. Tuy nhiên, các framework kiểm thử truyền thống như Jest thường dựa vào các công cụ bundling như Webpack hoặc các bộ chuyển đổi (transpiler) như Babel để xử lý code, dẫn đến thời gian setup và chạy test có thể chậm hơn, đặc biệt với các dự án lớn.

Vitest giải quyết vấn đề này bằng cách tích hợp sâu với pipeline của Vite. Nó sử dụng chính bộ giải quyết module, bộ chuyển đổi (nếu cần cho TypeScript/JSX), và hệ thống hot module replacement (HMR) của Vite. Điều này mang lại những lợi ích vượt trội:

  • Tốc độ đáng kinh ngạc: Nhờ tận dụng Vite và native ESM, Vitest có thể khởi động và chạy test cực nhanh. Chế độ watch mode phản hồi gần như tức thời khi code thay đổi.
  • Trải nghiệm phát triển liền mạch: Cấu hình tối thiểu, hoạt động tốt ngay khi cài đặt (zero-config) trong hầu hết các dự án Vite.
  • Tương thích cao: API kiểm thử của Vitest được thiết kế để tương thích với Jest, giúp các nhà phát triển đã quen thuộc với Jest có thể chuyển đổi dễ dàng. Nó cũng hoạt động tốt với các thư viện phổ biến như React Testing Library (RTL), Jest-DOM.

Nói cách khác, nếu bạn đang sử dụng Vite để xây dựng ứng dụng React của mình, Vitest là một lựa chọn kiểm thử tự nhiên và tối ưu nhất về mặt hiệu suất và trải nghiệm.

Bắt Đầu Với Vitest Trong Dự Án React

Để bắt đầu sử dụng Vitest trong dự án React (đã sử dụng Vite), quá trình setup cực kỳ đơn giản. Giả sử bạn đã có một dự án React tạo bằng Vite.

Đầu tiên, cài đặt các dependency cần thiết:

npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom

Giải thích các gói:

  • vitest: Core framework kiểm thử.
  • @testing-library/react: Thư viện kiểm thử React, tập trung vào hành vi người dùng (chúng ta đã học về nó trong bài trước).
  • @testing-library/jest-dom: Các custom matcher của Jest DOM (ví dụ: toBeInTheDocument()) rất hữu ích khi kiểm thử DOM element. Vitest hỗ trợ tốt các matcher này.
  • jsdom: Một môi trường giả lập DOM trong Node.js, cần thiết để React Testing Library có thể render và tương tác với các component React mà không cần trình duyệt thật.

Tiếp theo, chúng ta cần cấu hình Vitest một chút để nó biết cách xử lý các file React và môi trường DOM. Thông thường, bạn có thể thêm một file cấu hình vitest.config.js (hoặc .ts) ngay cạnh file vite.config.js của bạn. Tuy nhiên, với đa số dự án React sử dụng Vite, bạn chỉ cần thêm cấu hình cho Vitest ngay trong file vite.config.js:

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true, // Tùy chọn: cho phép sử dụng các hàm global như describe, it, expect
    environment: 'jsdom', // Sử dụng môi trường jsdom để giả lập DOM
    setupFiles: './src/setupTests.js', // Đường dẫn tới file setup (sẽ tạo ở bước tiếp theo)
    css: true, // Tùy chọn: Xử lý CSS (nếu component của bạn import CSS)
  },
})

Tạo file src/setupTests.js (hoặc bất kỳ tên và đường dẫn nào bạn đã cấu hình) để import các matcher của @testing-library/jest-dom:

// src/setupTests.js
import '@testing-library/jest-dom/vitest';

Cuối cùng, thêm script để chạy test vào file package.json:

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
  "preview": "vite preview",
  "test": "vitest", // Thêm dòng này
  "coverage": "vitest run --coverage" // Tùy chọn: để chạy và xem báo cáo coverage
}

Bây giờ bạn có thể chạy test bằng lệnh npm test (hoặc yarn test, pnpm test). Vitest sẽ chạy ở chế độ watch mode theo mặc định, tự động chạy lại test khi bạn thay đổi code hoặc file test.

Viết Test Đầu Tiên Với Vitest và React Testing Library

Vitest tương thích gần như hoàn hảo với API của Jest. Điều này có nghĩa là nếu bạn đã quen thuộc với describe, it (hoặc test), expect, và các matcher của Jest, bạn sẽ cảm thấy rất thoải mái khi viết test với Vitest.

Chúng ta hãy viết một vài ví dụ đơn giản.

Test một Pure Function

Tạo file src/utils/math.js:

// src/utils/math.js
export function sum(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

Tạo file test tương ứng src/utils/math.test.js:

// src/utils/math.test.js
import { describe, it, expect } from 'vitest'; // Không cần thiết nếu globals: true

import { sum, subtract } from './math';

describe('math utility functions', () => {
  it('should correctly sum two numbers', () => {
    expect(sum(2, 3)).toBe(5);
    expect(sum(-1, 5)).toBe(4);
    expect(sum(0, 0)).toBe(0);
  });

  it('should correctly subtract two numbers', () => {
    expect(subtract(5, 3)).toBe(2);
    expect(subtract(10, 20)).toBe(-10);
    expect(subtract(0, 0)).toBe(0);
  });
});

Chạy npm test, bạn sẽ thấy kết quả test được hiển thị nhanh chóng trong terminal.

Test một Component React

Sử dụng React Testing Library để test các component. Giả sử chúng ta có một component Button.jsx đơn giản:

// src/components/Button.jsx
import React from 'react';

function Button({ children, onClick }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

export default Button;

Tạo file test src/components/Button.test.jsx:

// src/components/Button.test.jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/vitest'; // Đảm bảo matcher như toBeInTheDocument hoạt động

import Button from './Button';

describe('Button component', () => {
  it('should render with the correct text', () => {
    render(<Button>Click Me</Button>);

    // Sử dụng các query của React Testing Library để tìm element
    const buttonElement = screen.getByText('Click Me');

    // Sử dụng matcher từ @testing-library/jest-dom
    expect(buttonElement).toBeInTheDocument();
  });

  it('should call the onClick handler when clicked', () => {
    // Tạo một mock function
    const handleClick = vi.fn();

    render(<Button onClick={handleClick}>Clickable</Button>);

    const buttonElement = screen.getByText('Clickable');

    // Giả lập sự kiện click
    fireEvent.click(buttonElement);

    // Kiểm tra xem mock function đã được gọi bao nhiêu lần
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('should be disabled if disabled prop is true (example extension)', () => {
    // Mặc dù component Button ở trên chưa có prop disabled,
    // đây là ví dụ minh họa cách kiểm thử prop
    const handleClick = vi.fn();
    render(<Button onClick={handleClick} disabled>Disabled Button</Button>); // Giả định Button có prop disabled

    const buttonElement = screen.getByText('Disabled Button');

    // Kiểm tra xem button có thuộc tính disabled hay không
    // expect(buttonElement).toBeDisabled(); // Cần component Button hỗ trợ prop disabled

    // Giả lập click vào button bị disable, handler không nên được gọi
    fireEvent.click(buttonElement);
    expect(handleClick).not.toHaveBeenCalled(); // Handler không được gọi
  });
});

Chạy npm test. Vitest sẽ tìm các file có đuôi .test.js, .test.jsx, .test.ts, .test.tsx (hoặc cấu hình khác trong file config) và chạy chúng. Kết quả sẽ cho thấy các test của bạn đã pass hay fail.

Các Tính Năng Nổi Bật Của Vitest

Vitest không chỉ nhanh mà còn đi kèm với nhiều tính năng hiện đại và hữu ích:

Hot Module Replacement (HMR) cho Test

Một trong những điểm mạnh nhất của Vitest là HMR. Khi bạn thay đổi code của component hoặc test, Vitest chỉ chạy lại các test bị ảnh hưởng bởi sự thay đổi đó một cách cực kỳ nhanh chóng. Điều này mang lại feedback loop (vòng phản hồi) gần như tức thời, cải thiện đáng kể năng suất làm việc.

API Tương Thích Với Jest

Nếu bạn đã quen với Jest, việc chuyển sang Vitest rất dễ dàng. Các hàm global như describe, it (hoặc test), expect, các matcher, và cả cú pháp mocking (vi.mock, vi.fn) đều tương tự như Jest. Điều này giảm thiểu đáng kể công sức học lại.

Mocking Mạnh Mẽ

Vitest cung cấp các công cụ mocking tích hợp sẵn mạnh mẽ (dưới namespace vi) cho phép bạn dễ dàng mock các module, hàm, hoặc thậm chí là toàn bộ các dependencies bên ngoài để kiểm thử các đơn vị code một cách cô lập.

// Ví dụ mock một module
import { describe, it, expect, vi } from 'vitest';
import { fetchData } from './api'; // Giả sử có hàm fetchData gọi API

// Mock toàn bộ module './api'
vi.mock('./api', () => ({
  fetchData: vi.fn(() => Promise.resolve({ data: 'mocked data' })),
}));

describe('data processing', () => {
  it('should process data fetched from API', async () => {
    const result = await fetchData(); // Gọi hàm đã bị mock
    expect(result).toEqual({ data: 'mocked data' });
    expect(fetchData).toHaveBeenCalledTimes(1); // Kiểm tra xem hàm mock đã được gọi chưa
  });
});

Snapshot Testing

Snapshot testing là một kỹ thuật giúp bạn kiểm tra xem giao diện người dùng của component có thay đổi bất ngờ hay không. Vitest hỗ trợ snapshot testing tương tự như Jest:

// src/components/Header.test.jsx
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';

import Header from './Header';

describe('Header component', () => {
  it('should render correctly', () => {
    const { asFragment } = render(<Header title="Welcome" />);

    // So sánh output render với snapshot lưu trữ
    expect(asFragment()).toMatchSnapshot();
  });
});

Lần đầu chạy test này, Vitest sẽ tạo một file .snap chứa cấu trúc render của component. Các lần chạy sau, nó sẽ so sánh output mới với file snapshot này. Nếu có sự khác biệt, test sẽ fail và bạn cần kiểm tra xem đó là thay đổi mong muốn (cập nhật snapshot bằng cách chạy npm test -u) hay là lỗi.

Báo Cáo Coverage Tích Hợp

Vitest có tích hợp sẵn tính năng báo cáo code coverage (mức độ code của bạn được kiểm thử). Bạn chỉ cần chạy lệnh npm run coverage (nếu đã thêm script như hướng dẫn ban đầu). Vitest sẽ tạo ra một báo cáo chi tiết (HTML, text, v.v.) cho biết dòng code nào đã được thực thi trong quá trình chạy test.

Vitest So Với Jest: Chọn Lựa Nào?

Đây là câu hỏi mà nhiều developer sẽ đặt ra. Cả hai đều là những framework kiểm thử tuyệt vời với cộng đồng lớn và nhiều tài nguyên. Tuy nhiên, có một số khác biệt chính cần cân nhắc, đặc biệt là trong bối cảnh các dự án hiện đại sử dụng Vite.

Dưới đây là bảng so sánh đơn giản:

Tính năng Vitest Jest
Tốc độ Rất nhanh (nhờ tận dụng Vite, Native ESM) Tốt, nhưng có thể chậm hơn ở startup và watch mode với dự án lớn (dựa trên Node/JSDOM, cần Babel/Webpack)
Thiết lập (Setup) Tối thiểu (Zero-config với Vite), tích hợp sâu với vite.config.js Cần cấu hình thêm (Babel, Webpack, preset Jest)
API Tương thích với Jest API API chuẩn của Jest
Tích hợp với Vite Bản xứ (Native) Cần plugin hoặc cấu hình đặc biệt
Hot Module Replacement (HMR) cho Test Có (nhanh) Không có
Mocking Tích hợp sẵn (vi) Tích hợp sẵn (jest.mock, jest.fn)
Snapshot Testing Có (tương thích Jest)
Code Coverage Tích hợp sẵn Tích hợp sẵn (cần cấu hình)
Hệ sinh thái & Cộng đồng Đang phát triển mạnh mẽ Rất lớn và trưởng thành

Khi nào nên chọn Vitest?

  • Bạn đang bắt đầu một dự án React mới và sử dụng Vite. Vitest sẽ là lựa chọn tự nhiên và hiệu quả nhất.
  • Bạn đang làm việc trên một dự án Vite hiện có và muốn thêm kiểm thử hoặc thay thế Jest để có tốc độ nhanh hơn.
  • Bạn muốn trải nghiệm phát triển test nhanh nhất có thể nhờ HMR cho test.
  • Bạn đã quen với Jest API và muốn một công cụ tương tự nhưng hiện đại và nhanh hơn.

Khi nào Jest vẫn là lựa chọn tốt?

  • Bạn đang làm việc trên một dự án lớn, lâu đời đã có sẵn hệ thống test với Jest và không có kế hoạch chuyển sang Vite. Việc di chuyển toàn bộ hệ thống test có thể tốn công sức.
  • Dự án của bạn không sử dụng Vite (ví dụ: Create React App – mặc dù đang dần chuyển sang Vite, hoặc các setup Webpack tùy chỉnh).
  • Bạn cần một tính năng rất đặc thù của Jest hoặc một integration với một công cụ/thư viện trong hệ sinh thái Jest mà Vitest chưa hỗ trợ đầy đủ (dù rất hiếm).

Trong bối cảnh “React Roadmap” hiện đại, khi Vite đang trở thành công cụ build mặc định được khuyến khích cho các dự án React mới, việc học và sử dụng Vitest trở nên ngày càng quan trọng. Nó đại diện cho cách tiếp cận kiểm thử mới, tận dụng tối đa hiệu suất của các công cụ phát triển hiện đại.

Tích Hợp Sâu Hơn Với React Testing Library và các Matcher

Như đã đề cập, Vitest hoạt động rất tốt với React Testing Library (đã được giới thiệu trong một bài trước). RTL cung cấp phương pháp kiểm thử các component React tập trung vào hành vi người dùng, thay vì chi tiết triển khai bên trong. Kết hợp Vitest (test runner) với RTL (phương pháp kiểm thử component) và @testing-library/jest-dom (các custom matcher cho DOM) tạo nên bộ ba mạnh mẽ để kiểm thử UI trong các ứng dụng React.

Việc sử dụng @testing-library/jest-dom mang lại các matcher rất trực quan khi làm việc với DOM, ví dụ:

expect(element).toBeInTheDocument(); // Kiểm tra element có tồn tại trong DOM không
expect(element).toBeVisible(); // Kiểm tra element có hiển thị không
expect(element).toBeDisabled(); // Kiểm tra element có bị disable không
expect(element).toHaveTextContent('...'); // Kiểm tra nội dung text của element
expect(element).toHaveAttribute('href', '...'); // Kiểm tra thuộc tính của element
// ... và nhiều matcher khác

Bạn chỉ cần đảm bảo đã import '@testing-library/jest-dom/vitest' trong file setup (setupTests.js) như đã hướng dẫn ở phần cài đặt.

Lời Kết

Kiểm thử là một phần không thể thiếu trên con đường trở thành một nhà phát triển React chuyên nghiệp. Nó giúp chúng ta xây dựng những ứng dụng đáng tin cậy, dễ bảo trì và tự tin hơn khi thay đổi code.

Chúng ta đã tìm hiểu qua Jest và React Testing Library như những công cụ kiểm thử tiêu chuẩn. Giờ đây, với sự xuất hiện của Vitest, các nhà phát triển làm việc với Vite có thêm một lựa chọn kiểm thử mạnh mẽ, nhanh chóng và mang lại trải nghiệm phát triển tuyệt vời.

Vitest không chỉ là một test runner nhanh hơn Jest trong môi trường Vite, mà còn là một công cụ hiện đại được thiết kế để tận dụng những tiến bộ trong hệ sinh thái frontend. Với API quen thuộc và tích hợp liền mạch với Vite, việc chuyển đổi hoặc bắt đầu với Vitest trở nên rất dễ dàng.

Hãy thử áp dụng Vitest vào dự án React+Vite tiếp theo của bạn và trải nghiệm sự khác biệt về tốc độ và sự tiện lợi mà nó mang lại. Kiểm thử hiệu quả sẽ giúp bạn tiết kiệm rất nhiều thời gian và công sức trong dài hạn.

Bài viết tiếp theo trong chuỗi React Roadmap sẽ khám phá các khía cạnh khác trên hành trình làm chủ React. Đừng quên theo dõi!

Chỉ mục