Ngày nay thì việc làm ra một ứng dụng web xịn xò thì ngoài việc code theo chuẩn thì để dự án có chất lượng hơn bằng việc đọc code dễ hiểu, nhanh chóng nắm được luồng xử lý, dễ bảo trì và mở rộng trong tương lai, hầu hết mọi người đều áp dụng thêm phần Testing cho dự án và đặc biệt việc này không thể thiếu trong các sản phẩm lớn và sẽ được làm và mở rộng theo thời gian dài trong tương lai. Bài viết này chúng ta sẽ đi khám khá làm thế nào để viết test cho React Hook bằng cách sử dụng Vitest và React Testing library sao cho để bảo trì và mở rộng
Cài đặt Vitest và JSdom
Được cung cấp bởi Vite, Vitest tuyên bố là “khung sườn kiểm thử đơn vị nhanh chóng” cho dự án khởi tạo từ Vite. Vitest cung cấp các chức năng và cú pháp tương tự như Jest, với TypeScript/JSX được hổ trợ đầy đủ đồng thời chế độ xem (HRM) và chạy kiểm thử của bạn nhanh hơn.
Mặc dù ban đầu được xây dựng cho dự án do Vite cung cấp, chúng ta cũng có thể sử dụng Vitest với một cấu hình phù hợp trong dự án không phải Vite, chẳng hạn Webpack.
Để cài đặt Vitest trong dự án React, chúng ta có thể cài đặt Vitest ở dạng thành phần thụ thuộc của nhà phát triển (devDependencies) với câu lệnh dưới đây
npm install -D vitest
Chúng ta cũng cài đặt jsdom
(or hoặc bất kì triển khai DOM chuẩn nào) dưới dạng devDependencies:
npm install -D jsdom
Sau đó, trong vitest.config.js (hoặc vite.config.js cho các dự án Vite), chúng ta thêm đối tượng thử nghiệm sau vào cấu hình, với jsdom làm môi trường DOM để thử nghiệm:
//... export default defineConfig({ test: { environment: 'jsdom', }, })
Chúng ta cũng có thể thiết lập trường global: true
như vậy chúng ta không cần phải nhập 1 cách rõ ràng cho mổi phương thức như expect
, describe
, it
, hoặc vi
instance từ gói vitest
trong mổi file test
Sau khi thực hiện xong với vitest.config.js
, chúng ta thêm 1 câu lệnh mới trong scripts
trong tệp package.json
cho việc chạy unit tests như sau:
"scripts": { "test:unit": "vitest --root src/" }
Trong đoạn mã trên, chúng tôi đặt thư mục gốc của các thử nghiệm là thư mục src/. Ngoài ra, chúng ta có thể đặt nó trong vite.config.js trong trường test.root. Cả hai cách tiếp cận đều hoạt động như nhau. Tiếp theo, hãy thêm một số bài kiểm tra.
Kiểm tra hook thông thường với React Testing Library
Vitest hỗ trợ kiểm tra mọi mã JavaScript và TypeScript. Tuy nhiên, để kiểm tra các tính năng dành riêng cho thành phần React như hook React, chúng ta vẫn cần tạo một trình bao bọc cho hook mong muốn và mô phỏng quá trình thực thi hook.
Để làm được như vậy, chúng ta có thể cài đặt và sử dụng Render hooks from React Testing Library:
yarn add -D @testing-library/react-hooks
Sau khi hoàn thành, chúng ta có thể sử dụng phương thức renderHook
từ package này để kết xuất hook mong muốn. renderHook
sẽ trả về 1 đối tượng chứa một thuộc tính result
và các phương thức hữu dụng khác như unmount
và waitFor
. Chúng ta sau đó có thể truy cập giá trị trả về của hook từ thuộc tính result.current
.
Ví dụ, chúng ta hãy nhìn vào một hook useSearch
mà nhận vào 1 mảng ban đầu và trả về 1 đối tưởng phản ứng searchTerm, một danh sách các mục đã được lọc, và một phương thức cập nhật cụm từ tìm kiếm. Việc thực hiện nó như sau:
//hooks/useSearch.ts import { useState, useMemo } from "react"; export const useSearch = (items: any[]) => { const [searchTerm, setSearchTerm] = useState(''); const filteredItems = useMemo( () => items.filter( movie => movie.title.toLowerCase().includes(searchTerm.toLowerCase()) ) , [items, searchTerm]); return { searchTerm, setSearchTerm, filteredItems }; }
Chúng ta có thể việc một kiểm thử để kiểm tra giá trị trả về mặc định của hook bao gồm searchTerm
và cho filterItems
như bên dưới:
import { expect, it, describe } from "vitest"; import { renderHook } from '@testing-library/react-hooks' import { useSearch } from "./useSearch" describe('useSearch', () => { it('should return a default search term and original items', () => { const items = [{ title: 'Star Wars' }]; const { result } = renderHook(() => useSearch(items)); expect(result.current.searchTerm).toBe(''); expect(result.current.filteredItems).toEqual(items); }); });
Để kiểm tra xem hook có hoạt động hay không khi chúng ta cập nhật searchTerm, chúng ta có thể sử dụng phương thức act() để mô phỏng quá trình thực thi setSearchTerm, như trong trường hợp kiểm tra bên dưới:
import { /**... */ act } from "vitest"; //... it('should return a filtered list of items', () => { const items = [ { title: 'Star Wars' }, { title: 'Starship Troopers' } ]; const { result } = renderHook(() => useSearch(items)); act(() => { result.current.setSearchTerm('Wars'); }); expect(result.current.searchTerm).toBe('Wars'); expect(result.current.filteredItems).toEqual([{ title: 'Star Wars' }]); }); //...
Lưu ý ở đây bạn không thể hủy cấu trúc các thuộc tính phản ứng của phiên bản result.current, nếu không chúng sẽ mất khả năng phản ứng. Ví dụ: đoạn mã dưới đây sẽ không hoạt động:
const { searchTerm } = result.current; act(() => { result.current.setSearchTerm('Wars'); }); expect(searchTerm).toBe('Wars'); // This won't work
Tiếp theo, chúng ta có thể chuyển sang thử nghiệm hook useMovies phức tạp hơn chứa logic bất đồng bộ.
Thử nghiệm hook với logic bất đồng bộ
Hãy xem ví dụ triển khai useMovies hook dưới đây:
export const useMovies = ():{ movies: Movie[], isLoading: boolean, error: any } => { const [movies, setMovies] = useState([]); const fetchMovies = async () => { try { setIsLoading(true); const response = await fetch("https://swapi.dev/api/films"); if (!response.ok) { throw new Error("Failed to fetch movies"); } const data = await response.json(); setMovies(data.results); } catch (err) { //do something } finally { //do something } }; useEffect(() => { fetchMovies(); }, []); return { movies } }
Trong đoạn mã ở trên, hook chạy phương thức bất đồng bộ fetchMovies
trong lần kết suất đầu tiên bằng sử dụng hook useEffect
. Việc triển khai này dẫn tới 1 vấn đề khi chúng ta cố gắng kiểm thử hook, vì phương thức renderHook
từ @testing-library/react-hooks
không đợi cuộc gọi bất đồng bộ kết thúc. Vì vậy chúng ta không biết khi nào quá trình nạp sẽ được giải quyết, chúng ta sẽ không thể biết được giá trị movies
trả về khi nó hoàn thành.
Để giải quyết điều đó, chúng ta có thể sử dụng phương thức waitFor
từ gói @testing-library/react-hooks
, như đoạn mã sau:
/**useMovies.test.ts */ describe('useMovies', () => { //... it('should fetch movies', async () => { const { result, waitFor } = renderHook(() => useMovies()); await waitFor(() => { expect(result.current.movies).toEqual([{ title: 'Star Wars' }]); }); }); //... });
waitFor
chấp nhận một callback và trả về một Promise mà giải quyết khi callback được thực thi thành công. Trong đoạn mã trên, chúng ta đợi cho giá trị movies
bằng với giá trị kì vọng. Tùy chọn, chúng ta có thể truyền một đối tượng làm đối số thứ hai cho waitFor
để cấu hình thời gian chờ và khoảng thời gian bỏ phiếu. Ví dụ, chúng ta có thể cấu hình thời gian chờ 1000ms như bên dưới:
await waitFor(() => { expect(result.current.movies).toEqual([{ title: 'Star Wars' }]); }, { timeout: 1000 });
Bằng việc làm như vậy, nếu giá trị movies
không bằng với giá trị kì vọng sau 1000ms, kiểm thử sẽ lỗi.
Theo dõi và kiểm tra cuộc gọi API bên ngoài bằng cách sử dụng spyOn và waitFor
Trong bài kiểm tra trước cho hook useMovies
, chúng ta đang tìm nạp dữ liệu bên ngoài bằng fetch
API, điều này không lý tưởng cho việc thử nghiệm đơn vị. Thay vì, chúng ta nên sử dụng phương thức vi.spyOn
(với vi
như một thể hiện của ViTest) để giả mạo trên phương thức global.fetch
và mô phỏng việc thực hiện nó để trả về 1 phản hồi giả, như đoạn mã bên dưới:
import { /**... */ vi, beforeAll } from "vitest"; describe('useMovies', () => { //Spy on the global fetch function const fetchSpy = vi.spyOn(global, 'fetch'); //Run before all the tests beforeAll(() => { //Mock the return value of the global fetch function const mockResolveValue = { ok: true, json: () => new Promise((resolve) => resolve({ results: [{ title: 'Star Wars' }] })) }; fetchSpy.mockReturnValue(mockResolveValue as any); }); it('should fetch movies', async () => { /**... */ } });
Trong đoạn mã phía trên, chúng ta giả định giá trị trả về của fetchSpy
sử dụng phương thức mockReturnValue()
của nó với giá trị chúng ta đã tạo. Với việc triển khai này, chúng ta có thể chạy thử nghiệm của chúng ta mà không phải kích hoạt cuộc giọ API thật, làm giảm cơ hội kiểm tra thất bại do yếu tố bên ngoài.
Và vì chúng tôi mô phỏng giá trị trả về của phương thức tìm nạp, chúng ta cần phải phục hồi triển khai ban đầu của nó sau khi thử nghiệm kết thúc, bằng cách sử dụng phương thức mockRestore
từ Vitest, như đoạn mã bên dưới:
import { /**... */ vi, beforeAll, afterAll } from "vitest"; describe('useMovies', () => { const fetchSpy = vi.spyOn(global, 'fetch'); /**... */ //Run after all the tests afterAll(() => { fetchSpy.mockRestore(); }); });
Ngoài ra, chúng ta cũng có thể sử dụng phương thức mockClear()
để xóa tất cả thông tin giả, chẳng hạn như số lượng cuộc gọi và kết quả của bản mô phỏng. Phương pháp này tiện dụng khi khẳng định các cuộc gọi mô phỏng hoặc mô phỏng cùng một chức năng với các giá trị trả về khác nhau cho các thử nghiệm khác nhau. Chúng ta thường sử dụng phương thức mockClear()
trong beforeEach
hoặc afterEach
để chắn chắn kiểm thử của chúng ta được cô lập 1 cách hoàn toàn.
Đó là tất cả. Bây giờ bạn có thể tiếp tục và thử nghiệm các móc tùy chỉnh của mình một cách hiệu quả.
Nguồn: https://mayashavin.com/articles/test-react-hooks-with-vitest
Happy testing 🙂