Hook тест
Custom hook бол React-н хамгийн хүчирхэг боломжуудын нэг — логикийг component-аас тусгаарлан, дахин ашиглагдах болгодог. Гэхдээ hook-ыг шууд функц дуудах шиг тест хийж болохгүй, учир нь hook нь зөвхөн React component дотор ажилладаг. renderHook хэрэгсэл нь энэ асуудлыг шийддэг — hook-ыг component-д байрлуулах шаардлагагүйгээр тест хийх боломж олгодог.
renderHook танилцуулга
@testing-library/react сангаас renderHook ба act импортолно:
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect } from "vitest";
renderHook — hook-ыг виртуал орчинд ажиллуулна
act — state өөрчлөлт, async үйлдлийг синхрончилна
Хамгийн энгийн hook-оос эхэлье:
// src/hooks/useCounter.ts
import { useState } from "react";
interface UseCounterOptions {
initialValue?: number;
step?: number;
min?: number;
max?: number;
}
function useCounter({
initialValue = 0,
step = 1,
min = -Infinity,
max = Infinity,
}: UseCounterOptions = {}) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount((c) => Math.min(c + step, max));
const decrement = () => setCount((c) => Math.max(c - step, min));
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
export default useCounter;
useCounter-ийн тест
// src/hooks/useCounter.test.ts
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import useCounter from "./useCounter";
describe("useCounter hook", () => {
it("анхны утгаар эхэлнэ", () => {
const { result } = renderHook(() => useCounter({ initialValue: 5 }));
expect(result.current.count).toBe(5);
});
it("increment дуудахад нэмэгдэнэ", () => {
const { result } = renderHook(() => useCounter());
// State өөрчлөлтийг act дотор хийх шаардлагатай
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it("step ажиллана", () => {
const { result } = renderHook(() => useCounter({ step: 5 }));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(10);
});
it("max хязгаараас хэтрэхгүй", () => {
const { result } = renderHook(() =>
useCounter({ initialValue: 9, max: 10 }),
);
act(() => {
result.current.increment();
result.current.increment(); // max-аас хэтэрч болохгүй
});
expect(result.current.count).toBe(10);
});
it("reset анхны утга руу буцаана", () => {
const { result } = renderHook(() => useCounter({ initialValue: 3 }));
act(() => {
result.current.increment();
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(3);
});
});
result.current нь hook-н буцаасан утгыг агуулна. State өөрчлөгдөх бүрт result.current автоматаар шинэчлэгдэнэ.
Async hook тест
Сервераас өгөгдөл татдаг hook-ыг тест хийхэд waitFor хэрэглэнэ:
// src/hooks/useCourse.ts
import { useState, useEffect } from "react";
interface Course {
slug: string;
title: string;
lessonCount: number;
}
function useCourse(slug: string) {
const [course, setCourse] = useState<Course | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setIsLoading(true);
fetch(`/api/courses/${slug}`)
.then((res) => {
if (!res.ok) throw new Error("Курс олдсонгүй");
return res.json();
})
.then(setCourse)
.catch((err: Error) => setError(err.message))
.finally(() => setIsLoading(false));
}, [slug]);
return { course, isLoading, error };
}
export default useCourse;
Тест файл:
// src/hooks/useCourse.test.ts
import { renderHook, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import useCourse from "./useCourse";
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe("useCourse hook", () => {
beforeEach(() => {
mockFetch.mockReset();
});
it("эхэндээ ачааллаж байгааг харуулна", () => {
mockFetch.mockImplementation(() => new Promise(() => {}));
const { result } = renderHook(() => useCourse("javascript"));
expect(result.current.isLoading).toBe(true);
expect(result.current.course).toBeNull();
});
it("амжилттай татвал курсийн мэдээлэл буцаана", async () => {
const mockCourse = {
slug: "javascript",
title: "JavaScript үндэс",
lessonCount: 20,
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockCourse,
});
const { result } = renderHook(() => useCourse("javascript"));
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.course).toEqual(mockCourse);
expect(result.current.error).toBeNull();
});
it("алдаа гарвал error state тохируулна", async () => {
mockFetch.mockResolvedValueOnce({ ok: false, status: 404 });
const { result } = renderHook(() => useCourse("404-course"));
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.error).toBe("Курс олдсонгүй");
expect(result.current.course).toBeNull();
});
it("slug өөрчлөгдөхөд дахин татна", async () => {
const jsData = { slug: "javascript", title: "JavaScript", lessonCount: 20 };
const tsData = { slug: "typescript", title: "TypeScript", lessonCount: 15 };
mockFetch
.mockResolvedValueOnce({ ok: true, json: async () => jsData })
.mockResolvedValueOnce({ ok: true, json: async () => tsData });
const { result, rerender } = renderHook(({ slug }) => useCourse(slug), {
initialProps: { slug: "javascript" },
});
await waitFor(() => expect(result.current.course?.slug).toBe("javascript"));
// slug өөрчлөх
rerender({ slug: "typescript" });
await waitFor(() => expect(result.current.course?.slug).toBe("typescript"));
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
rerender нь hook-д өгөгдсөн props-ыг өөрчилж дахин ажиллуулдаг — useEffect-н dependency array зөв ажиллаж байгааг шалгахад маш хэрэгтэй.
useLocalStorage hook тест
Browser API ашигладаг hook-ыг тест хийхэд mock хийх шаардлагатай:
// src/hooks/useLocalStorage.test.ts
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect, beforeEach } from "vitest";
import useLocalStorage from "./useLocalStorage";
describe("useLocalStorage hook", () => {
beforeEach(() => {
localStorage.clear();
});
it("анхны утгыг ашиглана", () => {
const { result } = renderHook(() => useLocalStorage("theme", "light"));
expect(result.current[0]).toBe("light");
});
it("утга тохируулахад localStorage-д хадгалдаг", () => {
const { result } = renderHook(() => useLocalStorage("theme", "light"));
act(() => {
result.current[1]("dark");
});
expect(result.current[0]).toBe("dark");
expect(localStorage.getItem("theme")).toBe('"dark"');
});
});
Hook тест бичихэд суралцах нь component тест бичихтэй адил чухал. Custom hook-уудыг тусдаа тест хийснээр кодын найдвартай байдал мэдэгдэхүйц нэмэгдэнэ — мөн hook-ын логикийг component-аас тусгаарлан бодох дадал хурцадна.
Дараагийн хичээлд:
React Profiler хэрэгслийг ашиглан аппын удаан хэсгийг олж, React.memo, useMemo, useCallback-ыг хэзээ яаж хэрэглэхийг практикаар судална.