IlmHamroh
JavaScript Full-stack/22-qism. Frontend testlash6/16-dars27 daqiqa
Mundarija (34)

React Testing Library III: custom render, providerlar, router va renderHook

Qisqacha: Haqiqiy komponent yolg'iz yashamaydi: unga QueryClientProvider, router, tema va til konteksti kerak. Ularsiz render darhol yiqiladi (No QueryClient set…). Yechim — custom render: bitta yordamchi funksiya (renderWithProviders, kanonda chiz) hamma providerni wrapper qilib o'raydi. Qoidalar: har testga yangi QueryClient (retry: false bilan), router — MemoryRouter initialEntries (yoki loader'li sahifa uchun createMemoryRouter), modul darajasidagi Zustand store — har testdan oldin getInitialState() ga qaytadi. Hook'ni komponentsiz sinash — renderHook (o'sha wrapper bilan).

Bu darsda

  • Provider yo'qligi xatolarini taniysiz va render ning wrapper sozlamasini ishlatasiz.
  • Custom render yozasiz: QueryClient, router, tema va i18n bitta joyda.
  • Testlar orasida holat "oqishi"ning uch manbasini (Query keshi, Zustand store, i18n tili) yopasiz.
  • MemoryRouter va createMemoryRouter bilan marshrutli komponentni sinaysiz.
  • renderHook bilan hookni sinaysiz va result.current tuzog'ini bilasiz.

Oldin bilishingiz kerak: RTL II, TanStack Query I, React Router I, Zustand I, Custom hooklar.

1. Nega bu kerak?

Kanon vazifalar ning RTL II dagi holatida beshta test faylining boshida deyarli bir xil 15 qator turibdi:

tsx
function Ilova({ children }: { children: ReactNode }) {
  const [mijoz] = useState(
    () =>
      new QueryClient({
        defaultOptions: { queries: { retry: false } },
      }),
  );
  return (
    <QueryClientProvider client={mijoz}>
      <BrowserRouter>
        <TemaProvider>{children}</TemaProvider>
      </BrowserRouter>
    </QueryClientProvider>
  );
}

Har fayl o'z nusxasini yozgan, ba'zisida TemaProvider tashqarida, birida staleTime: 30_000 qo'shilgan. Ertaga ilovaga yangi provider (masalan, bildirishnomalar) qo'shilsa — besh joyni topib tuzatish kerak. Bittasi esdan chiqsa, o'sha fayl testlari g'alati yiqiladi.

Bundan ham yomoni — ko'rinmas oqish: Zustand store modul darajasida bitta, bir test yozgan holat keyingisiga o'tadi. Testlar alohida o'tadi, birga yurganda yiqiladi — yoki aksincha.

O'xshatish: har oshpaz ishga kelib o'z pichog'ini, taxtasini, fartugini o'zi qidiradi. Yaxshi oshxonada esa har smena boshida stol bir xil tayyorlangan bo'ladi — toza va to'liq.

2. Provider yo'q — xato

2.1 Uchta tanish xabar

Provider'ga tayanadigan komponentni yolg'iz chizamiz:

jsx
import { render } from "@testing-library/react";
import { useQuery } from "@tanstack/react-query";
import { useLocation, useNavigate } from "react-router";
import { expect, test } from "vitest";

function DishCount() {
  const { data } = useQuery({
    queryKey: ["dishes"],
    queryFn: async () => ["Osh", "Manti"],
  });
  return <p>{data?.length ?? 0} ta taom</p>;
}

function CurrentPath() {
  return <p>{useLocation().pathname}</p>;
}

function BackButton() {
  const navigate = useNavigate();
  return <button onClick={() => navigate(-1)}>Orqaga</button>;
}

test("Query'siz", () => {
  expect(() => render(<DishCount />)).toThrow(
    "No QueryClient set, use QueryClientProvider to set one",
  );
});

test("router'siz — useLocation", () => {
  expect(() => render(<CurrentPath />)).toThrow(
    "useLocation() may be used only in the context of a <Router>",
  );
});

test("router'siz — useNavigate", () => {
  expect(() => render(<BackButton />)).toThrow(/useNavigate\(\)/);
});

Konsolda:

text
  ✓ Query'siz
  ✓ router'siz — useLocation
  ✓ router'siz — useNavigate
Testlar: 3 o'tdi (jami 3)

Tarjima: "QueryClient o'rnatilmagan, uni QueryClientProvider bilan bering"; "useLocation() faqat <Router> komponenti ichida ishlatilishi mumkin". Bu xatolar yaxshi — aniq aytadi. Yomoni — xato bermaydigan provider: tema konteksti createContext("light") sukut qiymati bilan jim ishlaydi, test esa doim "yorug'" temani ko'radi.

2.2 wrapper sozlamasi

render(ui, { wrapper }) — ui ni berilgan komponentga o'rab chizadi. rerender qilganda ham o'ram saqlanadi:

jsx
import { render, screen } from "@testing-library/react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";
import { MemoryRouter, useLocation } from "react-router";
import { expect, test } from "vitest";

function DishCount() {
  const { pathname } = useLocation();
  const { data, isPending } = useQuery({
    queryKey: ["dishes"],
    queryFn: async () => ["Osh", "Manti"],
  });
  if (isPending) return <p>Yuklanmoqda…</p>;
  return (
    <p>
      {pathname}: {data.length} ta taom
    </p>
  );
}

test("wrapper — Query va router", async () => {
  const client = new QueryClient();
  function Providers({ children }) {
    return (
      <QueryClientProvider client={client}>
        <MemoryRouter initialEntries={["/menyu"]}>
          {children}
        </MemoryRouter>
      </QueryClientProvider>
    );
  }
  render(<DishCount />, { wrapper: Providers });
  const text = await screen.findByText(/ta taom/);
  console.log(text.textContent);
  expect(screen.queryByText("Yuklanmoqda…")).toBeNull();
});

Konsolda:

text
/menyu: 2 ta taom
  ✓ wrapper — Query va router
Testlar: 1 o'tdi (jami 1)

client Providers tashqarisida yaratildi. Agar new QueryClient() ni Providers ichiga yozsangiz, har qayta chizishda yangi kesh paydo bo'ladi va ma'lumot yo'qoladi — kanondagi eski o'ram shuning uchun useState(() => new QueryClient(…)) qilgan edi.

MemoryRouter — manzil satrisiz, xotiradagi router (React Router I da natija oynasi uchun ishlatganmiz). Testda ham xuddi shu sabab: jsdom'da ham, natija oynasida ham haqiqiy manzil satri bizniki emas. initialEntries — boshlang'ich manzil.

3. Custom render

3.1 Bitta yordamchi

wrapper ni har testda yozish o'rniga — funksiya. Testing Library hujjati buni "custom render" deydi va odatda test-utils faylida saqlaydi.

flowchart TB
  T["test: renderWithProviders(&lt;Menu /&gt;, { route, theme })"] --> Q
  subgraph W["Wrapper — har chaqiruvda yangi"]
    Q["QueryClientProvider<br/>(yangi client, retry: false)"] --> R["MemoryRouter<br/>initialEntries: [route]"]
    R --> C["ThemeContext value={theme}"]
    C --> U["&lt;Menu /&gt; — sinalayotgan komponent"]
  end
  T -.->|"qaytaradi"| O["{ client, ...render natijasi }"]

Rasmda: test faqat komponentni va farq qiladigan sozlamani beradi; o'ram (provider'lar zanjiri) yordamchi ichida quriladi. Kod:

jsx
import { render, screen } from "@testing-library/react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";
import { createContext, use } from "react";
import { MemoryRouter, useSearchParams } from "react-router";
import { expect, test } from "vitest";

const ThemeContext = createContext("light");

function createTestClient() {
  return new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });
}

function renderWithProviders(
  ui,
  { route = "/", theme = "light", client = createTestClient() } = {},
) {
  function Wrapper({ children }) {
    return (
      <QueryClientProvider client={client}>
        <MemoryRouter initialEntries={[route]}>
          <ThemeContext value={theme}>{children}</ThemeContext>
        </MemoryRouter>
      </QueryClientProvider>
    );
  }
  return { client, ...render(ui, { wrapper: Wrapper }) };
}

const MENU = { hot: ["Osh", "Lag'mon"], cold: ["Ko'k choy"] };

function Menu() {
  const theme = use(ThemeContext);
  const [params] = useSearchParams();
  const type = params.get("tur") ?? "hot";
  const { data } = useQuery({
    queryKey: ["menu", type],
    queryFn: async () => MENU[type],
  });
  return (
    <ul aria-label="Menyu" className={theme}>
      {data?.map((d) => <li key={d}>{d}</li>)}
    </ul>
  );
}

test("manzil va tema — sozlama bilan", async () => {
  renderWithProviders(<Menu />, {
    route: "/?tur=cold",
    theme: "dark",
  });
  const list = screen.getByRole("list", { name: "Menyu" });
  expect(await screen.findByText("Ko'k choy")).toBeInTheDocument();
  expect(list).toHaveClass("dark");
});

test("kesh — test o'z client'ini ko'radi", async () => {
  const { client } = renderWithProviders(<Menu />);
  await screen.findByText("Osh");
  console.log(client.getQueryData(["menu", "hot"]));
});

Konsolda:

text
  ✓ manzil va tema — sozlama bilan
(2) ['Osh', "Lag'mon"]
  ✓ kesh — test o'z client'ini ko'radi
Testlar: 2 o'tdi (jami 2)

Uch qaror:

  • Sukut bo'yicha — eng ko'p kerak bo'lgani: route = "/", theme = "light". Test faqat farq qiladigan narsani yozadi: { route: "/?tur=cold", theme: "dark" }.
  • Har chaqiruvda yangi client — sukut qiymat createTestClient() har safar qayta hisoblanadi (JavaScript'da sukut parametr har chaqiruvda baholanadi). Kerak bo'lsa test o'zinikini beradi.
  • client qaytariladi — test keshga qarashi yoki uni oldindan to'ldirishi mumkin (client.setQueryData(…)).

<ThemeContext value={theme}> — React 19 da kontekstning o'zi provider (Context I).

3.2 Loyihada: test-utils fayli

Loyihada yordamchi alohida faylga chiqariladi va Testing Library'ning hamma eksportini qayta eksport qiladi — testlar bitta joydan import qiladi:

tsx
// tekshiruv/test-utils.tsx
import { render } from "@testing-library/react";
import type { RenderOptions } from "@testing-library/react";
import type { ReactElement } from "react";

export * from "@testing-library/react";

export function renderWithProviders(
  ui: ReactElement,
  options: Options & Omit<RenderOptions, "wrapper"> = {},
) {
  // … Wrapper yuqoridagidek
}

Omit<RenderOptions, "wrapper"> — qolgan render sozlamalari (container, baseElement) o'tadi, wrapper esa bizniki. Kanon xuddi shunday qildi — «Vazifalar qadami» da.

Tekshirib ko'ring: Hamma testlar bitta umumiy const client = new QueryClient() dan foydalansa nima bo'ladi?

Javob

Birinchi test yuklagan ma'lumot keshda qoladi. Ikkinchi test queryFn chaqirilishini kutsa — chaqirilmaydi; boshqa javobni kutsa — eskisini ko'radi. Testlar tartibga bog'liq bo'lib qoladi. Keyingi bo'limda buni jonli ko'ramiz.

4. Query: ikki tuzoq

4.1 Umumiy kesh

Ikki test bitta sharedClient dan foydalanadi. Natijada ikki qatorga qarang: 2-test ekranda qaysi narxni ko'rdi va load necha marta chaqirildi.

jsx
import { render, screen } from "@testing-library/react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";
import { expect, test, vi } from "vitest";

const sharedClient = new QueryClient();

function Price({ load }) {
  const { data } = useQuery({ queryKey: ["price"], queryFn: load });
  return <p>Osh: {data ?? "…"}</p>;
}

function renderShared(ui) {
  return render(
    <QueryClientProvider client={sharedClient}>
      {ui}
    </QueryClientProvider>,
  );
}

test("1: narx 35 000", async () => {
  renderShared(<Price load={async () => "35 000"} />);
  expect(await screen.findByText("Osh: 35 000")).toBeInTheDocument();
});

test("2: narx o'zgardi — 38 000", async () => {
  const load = vi.fn(async () => "38 000");
  renderShared(<Price load={load} />);
  console.log(screen.getByRole("paragraph").textContent);
  console.log("load chaqirildi:", load.mock.calls.length);
});

Konsolda:

text
  ✓ 1: narx 35 000
Osh: 35 000
load chaqirildi: 1
  ✓ 2: narx o'zgardi — 38 000
Testlar: 2 o'tdi (jami 2)

Nima bo'ldi? Qadamma-qadam:

Qadam Umumiy keshda Ekranda / Konsolda
1-test tugadi ["price"] = "35 000" ✓ 1: narx 35 000
2-test: render o'sha yozuv, eskirgan (stale) darhol Osh: 35 000
2-test: fonda load() yangi so'rov ketdi load chaqirildi: 1
2-test tugadi "38 000" hali yo'lda test eski narxni o'qib bo'lgan

Ikkinchi test o'z load ini chaqirdi, lekin ekranda birinchi testning narxi turibdi: kesh eski javobni darhol ko'rsatdi va yangisini fonda so'radi (TanStack Query I dagi stale-while-revalidate). "Avval yuklanish yozuvi" yoki "darhol 38 000" ni kutgan test yiqiladi — lekin faqat boshqa test bilan birga. Test 2 ni alohida yurgizsangiz, natija boshqa. Bu — tartibga bog'liq test, eng qiyin topiladigan xato turi. Davosi — custom render'dagi kabi har testga yangi QueryClient.

4.2 Sukutdagi uch marta qayta urinish

TanStack Query xato bergan so'rovni sukut bo'yicha 3 marta qayta urinadi, orasida kutib (1 s, 2 s, 4 s — TanStack Query I). Testda bu findBy ning 1 soniyasidan uzun:

jsx
import { render, screen } from "@testing-library/react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";
import { expect, test } from "vitest";

function Orders() {
  const { isError, failureCount } = useQuery({
    queryKey: ["orders"],
    queryFn: async () => {
      throw new Error("500");
    },
  });
  if (isError) return <p role="alert">Buyurtmalar yuklanmadi</p>;
  return <p role="status">Urinish: {failureCount}</p>;
}

function renderWith(client) {
  render(
    <QueryClientProvider client={client}>
      <Orders />
    </QueryClientProvider>,
  );
}

test("sukut: retry 3 — 1 s ichida xato ko'rinmaydi", async () => {
  renderWith(new QueryClient());
  await expect(screen.findByRole("alert")).rejects.toThrow(
    'Unable to find role="alert"',
  );
  console.log(screen.getByRole("status").textContent);
});

test("retry: false — xato darhol", async () => {
  const client = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  renderWith(client);
  expect(await screen.findByRole("alert")).toHaveTextContent(
    "Buyurtmalar yuklanmadi",
  );
});

Konsolda:

text
Urinish: 1
  ✓ sukut: retry 3 — 1 s ichida xato ko'rinmaydi
  ✓ retry: false — xato darhol
Testlar: 2 o'tdi (jami 2)

1 soniyada so'rov bir marta yiqildi va ikkinchi urinishni kutib turibdi. Sukutdagi jadval:

Vaqt Nima bo'ladi Ekranda
0 s 1-urinish yiqildi Urinish: 1
~1 s findByRole("alert") kutishni to'xtatdi — test shu yerda Urinish: 1
1 s, 3 s 2- va 3-urinish (oraliq 1 s, keyin 2 s) Urinish: 2, 3
7 s 4-urinish ham yiqildi (oraliq 4 s) «Buyurtmalar yuklanmadi»

Xato holati 7 soniyadan keyin kelardi. Shuning uchun test client'ida retry: false — "xato bo'lsa, darhol ko'rsat". Kanonning chiz() ida mutatsiyalar uchun ham shunday (mutations: { retry: false }).

5. Router bilan test

5.1 MemoryRouter va o'tish

Havola bosilganda boshqa sahifa ochilishini sinash uchun testda Routes ham bo'lishi kerak:

jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
  Link,
  MemoryRouter,
  Route,
  Routes,
  useLocation,
  useParams,
} from "react-router";
import { expect, test } from "vitest";

function MenuPage() {
  return (
    <nav aria-label="Taomlar">
      <Link to="/taom/osh">Osh</Link>
      <Link to="/taom/manti">Manti</Link>
    </nav>
  );
}

function DishPage() {
  const { slug } = useParams();
  return <h1>Taom: {slug}</h1>;
}

function LocationDisplay() {
  const { pathname } = useLocation();
  return <output aria-label="Manzil">{pathname}</output>;
}

function renderApp(route) {
  render(
    <MemoryRouter initialEntries={[route]}>
      <Routes>
        <Route path="/" element={<MenuPage />} />
        <Route path="/taom/:slug" element={<DishPage />} />
      </Routes>
      <LocationDisplay />
    </MemoryRouter>,
  );
}

test("havola — taom sahifasi", async () => {
  const user = userEvent.setup();
  renderApp("/");
  await user.click(screen.getByRole("link", { name: "Manti" }));
  expect(
    screen.getByRole("heading", { name: "Taom: manti" }),
  ).toBeInTheDocument();
  const where = screen.getByRole("status", { name: "Manzil" });
  console.log(where.textContent);
});

test("to'g'ridan manzil bilan", () => {
  renderApp("/taom/osh");
  expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
    "Taom: osh",
  );
});

Konsolda:

text
/taom/manti
  ✓ havola — taom sahifasi
  ✓ to'g'ridan manzil bilan
Testlar: 2 o'tdi (jami 2)

LocationDisplay — faqat test uchun kichik "ko'zgu": joriy manzilni ekranga chiqaradi, test uni rol bilan o'qiydi. Ikkinchi test — chuqur havola (Router III): sahifani to'g'ridan manzil bilan ochish ham ishlashi kerak.

Kanon ikki xil router ishlatadi: MemoryRouter (sukut) va BrowserRouter — testi location.search ni, ya'ni jsdom'ning haqiqiy window.location ini tekshiradigan holatlar uchun (filtr URL'da). BrowserRouter bilan har testdan oldin history.replaceState(null, "", "/") — aks holda oldingi testning manzili qoladi.

5.2 Loader'li sahifa: createMemoryRouter

Router darsidagi data mode — marshrut o'z loader i bilan. Uni MemoryRouter ga o'rab bo'lmaydi: router marshrutlarni o'zi bilishi kerak. Testda createMemoryRouter + RouterProvider:

jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
  createMemoryRouter,
  Link,
  RouterProvider,
  useLoaderData,
} from "react-router";
import { expect, test } from "vitest";

const DISHES = { 1: "Osh — 35 000", 2: "Manti — 30 000" };

function Dish() {
  const dish = useLoaderData();
  return (
    <main>
      <h1>{dish}</h1>
      <Link to="/taom/2">Keyingi</Link>
    </main>
  );
}

const routes = [
  {
    path: "/taom/:id",
    loader: async ({ params }) => DISHES[params.id],
    element: <Dish />,
    HydrateFallback: () => <p>Yuklanmoqda…</p>,
  },
];

test("loader ma'lumoti va o'tish", async () => {
  const user = userEvent.setup();
  const router = createMemoryRouter(routes, {
    initialEntries: ["/taom/1"],
  });
  render(<RouterProvider router={router} />);
  expect(
    await screen.findByRole("heading", { name: "Osh — 35 000" }),
  ).toBeInTheDocument();
  await user.click(screen.getByRole("link", { name: "Keyingi" }));
  await screen.findByRole("heading", { name: "Manti — 30 000" });
  console.log(router.state.location.pathname);
});

Konsolda:

text
/taom/2
  ✓ loader ma'lumoti va o'tish
Testlar: 1 o'tdi (jami 1)

router.state.location — testda router obyektining o'ziga qarash mumkin: "LocationDisplay" kerak emas. Lekin createMemoryRouter custom render'ga sig'maydi — u marshrutlar ro'yxatini talab qiladi, har sahifaning o'zi. Kanon ham shu qarorga keldi (pastda).

HydrateFallback — loader tugaguncha ko'rinadigan narsa. Uni bermasangiz, test o'tadi, lekin konsolda ogohlantirish chiqadi: No HydrateFallback element provided to render during initial hydration ("birinchi yuklanishda ko'rsatish uchun HydrateFallback berilmagan").

6. Til va tema: global holatni tiklash

i18n odatda bir marta — sozlash faylida ishga tushiriladi (kanonda vitest-sozlash.ts). Til esa global: bir test tilni almashtirsa, keyingi test boshqa tilda boshlanadi. i18next ichki resources bilan sinxron ishga tushadi (i18n I):

jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import i18n from "i18next";
import { initReactI18next, useTranslation } from "react-i18next";
import { beforeEach, expect, test } from "vitest";

i18n.use(initReactI18next).init({
  lng: "uz",
  fallbackLng: "uz",
  resources: {
    uz: { translation: { order: "Buyurtma berish" } },
    ru: { translation: { order: "Заказать" } },
  },
});

function OrderButton() {
  const { t, i18n: inst } = useTranslation();
  return (
    <div>
      <button onClick={() => inst.changeLanguage("ru")}>RU</button>
      <button>{t("order")}</button>
    </div>
  );
}

beforeEach(async () => {
  await i18n.changeLanguage("uz"); // har test — o'zbekcha
});

test("tilni almashtirish", async () => {
  const user = userEvent.setup();
  render(<OrderButton />);
  await user.click(screen.getByRole("button", { name: "RU" }));
  console.log(i18n.language);
});

test("keyingi test yana o'zbekchada", () => {
  render(<OrderButton />);
  expect(
    screen.getByRole("button", { name: "Buyurtma berish" }),
  ).toBeInTheDocument();
});

Konsolda:

text
ru
  ✓ tilni almashtirish
  ✓ keyingi test yana o'zbekchada
Testlar: 2 o'tdi (jami 2)

beforeEach ni olib tashlasangiz, ikkinchi test ruscha tugmani ko'radi va yiqiladi. Qoida hamma global holat uchun bir xil: test o'zgartirgan har narsa keyingi testdan oldin joyiga qaytsin — til, localStorage, history, soxta taymerlar, Zustand.

Tema bilan oson: provider'ni wrapper da qiymat bilan bering («Bitta yordamchi» dagidek theme: "dark"). Kanondagi TemaProvider esa tizim temasini matchMedia dan o'qiydi — jsdom'da matchMedia yo'q, sozlash faylida doim "yorug'" qaytaradigan soxtasi turibdi (RTL I).

7. renderHook: hookni komponentsiz sinash

7.1 result.current

Custom hooklar darsida hook ichiga mantiq chiqardik. Uni komponentsiz sinash — renderHook. U ko'rinmas test komponentini chizadi va hook natijasini result.current ga qo'yadi:

jsx
import { act, renderHook } from "@testing-library/react";
import { useState } from "react";
import { expect, test } from "vitest";

function useCart(initial = []) {
  const [items, setItems] = useState(initial);
  const add = (dish) => setItems((list) => [...list, dish]);
  return { items, add, count: items.length };
}

test("qo'shish — act ichida", () => {
  const { result } = renderHook(() => useCart());
  act(() => result.current.add("Osh"));
  act(() => result.current.add("Manti"));
  expect(result.current.items).toEqual(["Osh", "Manti"]);
});

test("boshlang'ich qiymat va rerender", () => {
  const { result, rerender } = renderHook(
    ({ start }) => useCart(start),
    { initialProps: { start: ["Choy"] } },
  );
  rerender({ start: [] });
  console.log(result.current.count);
});

test("tuzoq: result.current ni oldindan ochish", () => {
  const { result } = renderHook(() => useCart());
  const { items, add } = result.current;
  act(() => add("Osh"));
  console.log(items.length, result.current.items.length);
});

Konsolda:

text
  ✓ qo'shish — act ichida
1
  ✓ boshlang'ich qiymat va rerender
0 1
  ✓ tuzoq: result.current ni oldindan ochish
Testlar: 3 o'tdi (jami 3)
  • act — hook holatni o'zgartirsa, yangilanish tugashini kutadi (RTL I).
  • rerender — hookni yangi argumentlar bilan qayta chaqiradi. useState(initial) boshlang'ich qiymatni faqat birinchi chizishda oladi — shuning uchun count 1 qoldi. Bu test hookning shu xulqini qotiradi.
  • Tuzoq: const { items } = result.current — o'sha lahzadagi nusxa. Yangilanishdan keyin items eski (0), result.current.items — yangi (1). Har tekshiruvda result.current ni qaytadan o'qing.

7.2 Provider kerak bo'lgan hook

Hook useQuery yoki useLocation ishlatsa — renderHook ga ham wrapper:

jsx
import { renderHook, waitFor } from "@testing-library/react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";
import { expect, test } from "vitest";

function useDishes(load) {
  return useQuery({ queryKey: ["dishes"], queryFn: load });
}

function makeWrapper() {
  const client = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return ({ children }) => (
    <QueryClientProvider client={client}>
      {children}
    </QueryClientProvider>
  );
}

test("muvaffaqiyat", async () => {
  const { result } = renderHook(
    () => useDishes(async () => ["Osh", "Manti"]),
    { wrapper: makeWrapper() },
  );
  console.log(result.current.status);
  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toEqual(["Osh", "Manti"]);
});

test("xato", async () => {
  const load = async () => {
    throw new Error("Server 500");
  };
  const { result } = renderHook(() => useDishes(load), {
    wrapper: makeWrapper(),
  });
  await waitFor(() => expect(result.current.isError).toBe(true));
  console.log(result.current.error.message);
});

Konsolda:

text
pending
  ✓ muvaffaqiyat
Server 500
  ✓ xato
Testlar: 2 o'tdi (jami 2)

Hook natijasini kutish uchun findBy yo'q — ekran yo'q. Shuning uchun waitFor + result.current (RTL II).

Qachon renderHook, qachon komponent testi? Hook kutubxona yoki ko'p komponentda ishlatiladigan mantiq bo'lsa — renderHook. Bitta komponentning ichki ishi bo'lsa — komponentni foydalanuvchi kabi sinang: hook — implementatsiya tafsiloti (Nega test yozamiz).

8. Zustand: store'ni har testda tiklash

Zustand I da va'da qilgan edik: store modul darajasida bitta, bir test o'zgartirgan holat keyingisiga o'tadi. Mana o'sha oqish va davosi:

jsx
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, test } from "vitest";
import { create } from "zustand";

const useOrder = create((set) => ({
  items: [],
  add: (dish) => set((s) => ({ items: [...s.items, dish] })),
}));

describe("tiklashsiz", () => {
  test("1: osh qo'shildi", () => {
    const { result } = renderHook(() => useOrder());
    act(() => result.current.add("Osh"));
    expect(result.current.items).toEqual(["Osh"]);
  });

  test("2: yangi buyurtma bo'sh boshlanadi", () => {
    const { result } = renderHook(() => useOrder());
    expect(result.current.items.length).toBe(0);
  });
});

describe("tiklash bilan", () => {
  beforeEach(() => {
    useOrder.setState(useOrder.getInitialState(), true);
  });

  test("3: yangi buyurtma bo'sh boshlanadi", () => {
    const { result } = renderHook(() => useOrder());
    expect(result.current.items.length).toBe(0);
  });
});

Konsolda:

text
  ✓ tiklashsiz > 1: osh qo'shildi
  × tiklashsiz > 2: yangi buyurtma bo'sh boshlanadi
    → expected 1 to be +0 // Object.is equality
  ✓ tiklash bilan > 3: yangi buyurtma bo'sh boshlanadi
Testlar: 1 yiqildi, 2 o'tdi (jami 3)

2-test birinchi testning oshini ko'rdi — savatda 1 ta taom. (+0 — Vitest nolni ishorasi bilan yozadi: Object.is(0, -0) false.) getInitialState() — create() ga berilgan boshlang'ich holat (Zustand 5), ikkinchi argument true — almashtirish (birlashtirish emas): ortiqcha maydonlar ham ketadi.

Zustand hujjati boshqa yo'lni ham ko'rsatadi: __mocks__/zustand.ts da create ni o'rab, har testdan keyin hamma store'ni avtomat tiklash (vi.mock("zustand")). Kanon oddiyrog'ini tanladi — store'lar ikkita, tiklash custom render ichida.

9. Vazifalar qadami: chiz() va hookniChiz() (holat 06)

9.1 Branch va fayl

bash
git switch -c test/chiz-yordamchisi

Yangi paket yo'q. Yangi fayl — tekshiruv/yordamchi/chiz.tsx. Boshidagi izoh muammoni o'zi aytadi:

tsx
// yordamchi/chiz.tsx — 22/#06: providerlar bilan render (custom
// render). Ilova komponentlari kontekstga tayanadi: Query keshi,
// router, tema, i18n (vitest-sozlash.ts da bir marta), Zustand
// store'lari. Ilgari har test fayli o'z "Ilova" o'ramini yozardi
// (5 faylda deyarli bir xil) — endi bitta joyda. Har chaqiruv —
// YANGI QueryClient (testlar bir-birining keshini ko'rmasin),
// store'lar boshlang'ich holatga qaytadi

Sozlamalar turi va store tiklash:

tsx
type Sozlama = {
  /** "xotira" — MemoryRouter (yol bilan); "brauzer" — haqiqiy
   * location/history (URL'ni tekshiradigan testlar uchun) */
  router?: "xotira" | "brauzer" | undefined;
  /** xotira router'ining boshlang'ich manzili */
  yol?: string | undefined;
  mijoz?: QueryClient | undefined;
};

/** Zustand store'lari modul darajasida — testlar orasida holat
 * qoladi. getInitialState() — create() dagi boshlang'ich holat
 * (Zustand 5) */
function storelarniTikla(): void {
  useIlovaHolati.setState(useIlovaHolati.getInitialState(), true);
  useIzohlar.setState(useIzohlar.getInitialState(), true);
}

O'ram va ikki eksport:

tsx
function oramYasa({ router = "xotira", yol = "/", mijoz }: Sozlama) {
  const keshi = mijoz ?? yangiMijoz();
  return function Oram({ children }: { children: ReactNode }) {
    const ichki = <TemaProvider>{children}</TemaProvider>;
    return (
      <QueryClientProvider client={keshi}>
        {router === "brauzer" ? (
          <BrowserRouter>{ichki}</BrowserRouter>
        ) : (
          <MemoryRouter initialEntries={[yol]}>{ichki}</MemoryRouter>
        )}
      </QueryClientProvider>
    );
  };
}

export function chiz(
  ui: ReactElement,
  sozlama: Sozlama & Omit<RenderOptions, "wrapper"> = {},
) {
  const { router, yol, mijoz, ...qolgan } = sozlama;
  storelarniTikla();
  return render(ui, {
    wrapper: oramYasa({ router, yol, mijoz }),
    ...qolgan,
  });
}

export function hookniChiz<N>(hook: () => N, sozlama: Sozlama = {}) {
  storelarniTikla();
  return renderHook(hook, { wrapper: oramYasa(sozlama) });
}

yangiMijoz() — darsdagi createTestClient ning o'zi (retry: false so'rov va mutatsiyaga). i18n o'ramda yo'q — u sozlash faylida bir marta ishga tushadi.

9.2 Birinchi yurgizish — ikki xato

tip (tsc -b) yiqildi:

text
tekshiruv/yordamchi/chiz.tsx(68,23): error TS2379: Argument of type '{ router: "brauzer" | "xotira" | undefined; yol: string | undefined; mijoz: QueryClient | undefined; }' is not assignable to parameter of type 'Sozlama' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
  Types of property 'router' are incompatible.
    Type '"brauzer" | "xotira" | undefined' is not assignable to type '"brauzer" | "xotira"'.
      Type 'undefined' is not assignable to type '"brauzer" | "xotira"'.

Tarjima: "exactOptionalPropertyTypes: true bilan bu obyekt Sozlama ga mos emas. Maqsad maydonlariga undefined qo'shing." Sabab: chiz ichida const { router } = sozlama — router ning turi … | undefined. Uni { router } qilib uzatganda maydon bor, lekin qiymati undefined. Bu bayroq (Obyekt turlari) "maydon yo'q" va "maydon bor, undefined" ni farqlaydi. Tuzatish — yuqoridagi turdagi | undefined.

knip ham yiqildi:

text
Unused exports (2)
yangiMijoz       function  tekshiruv/yordamchi/chiz.tsx:28:17
storelarniTikla  function  tekshiruv/yordamchi/chiz.tsx:40:17

Ikki ichki yordamchi export bilan yozilgan, lekin hech kim import qilmagan — export olib tashlandi. Ikkala tekshiruv ham testlar emas, lekin test kodi ham kod: u ham turlar va o'lik kod qoidalaridan o'tadi.

9.3 Ko'chirish

App.test.tsx dan to'qqizta chaqiruv shunday o'zgardi:

diff
-    render(
-      <TemaProvider>
-        <App boshlangichRoyxat={ROYXAT} qoralama="" />
-      </TemaProvider>,
-      { wrapper: Ilova },
-    );
+    chiz(<App boshlangichRoyxat={ROYXAT} qoralama="" />, {
+      router: "brauzer",
+    });

Fayl 264 → 215 qator. harakatlar va qoralama testlari ham chiz ga o'tdi. Ataylab qoldirilganlari: VazifaSahifasi, VazifaTahriri, Auth, Menyu — ular createMemoryRouter (data mode) bilan o'z marshrutlarini quradi. Loader'li router chiz ga sig'maydi («Loader'li sahifa» dagi sabab), majburlab sig'dirish yordamchini murakkablashtirardi.

9.4 providerli-hooklar.test.tsx (6 test)

tsx
describe("Zustand store — testlar orasida tiklanadi", () => {
  test("1: izoh yoziladi", () => {
    const { result } = hookniChiz(() => useIzohlar());
    act(() => result.current.izohniYoz(1, "Bozordan"));
    expect(result.current.izohlar).toEqual({ 1: "Bozordan" });
  });

  test("2: oldingi testning izohi yo'q (storelarniTikla)", () => {
    const { result } = hookniChiz(() => useIzohlar());
    expect(result.current.izohlar).toEqual({});
  });
});

Kanon buni sinab ko'rdi: storelarniTikla() qatori olib tashlansa, 2-test yiqiladi — darsdagi «Zustand» bo'limi tajribasi aynan. Qolgan to'rtta: useUrlTanlovi (MemoryRouter: manzildan o'qish, noto'g'ri qiymat — standart; yozganda manzil o'zgaradi — tekshiruv useLocation() ni hook bilan birga chizib), useVazifalar (Query: server javobi — tayyor, 500 — xato).

Ikkinchi useUrlTanlovi testi qiziq usul ishlatadi — bitta renderHook da ikki hook:

tsx
    const { result } = hookniChiz(
      () => ({ url: useUrlTanlovi(), joy: useLocation() }),
      { yol: "/?filtr=faol" },
    );
    act(() => result.current.url.saralashniYoz("alifbo"));
    expect(result.current.joy.search).toBe(
      "?filtr=faol&saralash=alifbo",
    );

useLocation() — «MemoryRouter va o'tish» dagi LocationDisplay ning hook shakli: router'ning ichiga qarash uchun.

9.5 Natija va commit

Testlar 343/343 (337 + 6), npm test 11.67 s; test:qamrov — chegaradan o'tdi, satr qamrovi 62.76 % (holat 04 da 61.28 %). tip, lint, knip — toza. JS 206.7 KB (ilova kodi o'zgarmadi).

bash
git add -A
git commit

Commit (kanon): test: chiz() va hookniChiz() — Query, router, tema, store tiklash bitta joyda.

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
No QueryClient set… provider yo'q custom render / wrapper
useLocation() may be used only in the context of a <Router> router yo'q MemoryRouter o'ramda
test yolg'iz o'tadi, birga yiqiladi umumiy QueryClient, store, til har testga yangi client; beforeEach da tiklash
xato holati findBy da topilmaydi retry sukut bo'yicha 3 test client'ida retry: false
wrapper da new QueryClient() — kesh yo'qoladi har chizishda yangi client client'ni o'ramdan tashqarida yarating
result.current eski qiymat oldindan destrukturatsiya har safar result.current.x
loader ishlamaydi MemoryRouter data mode emas createMemoryRouter + RouterProvider
TS2379 … exactOptionalPropertyTypes undefined qiymatli maydon uzatilgan turga | undefined

11. Mashqlar

Mashqlar kurs/mashqlar/22/06-providerlar/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • render ga provider o'rami: render(ui, { : Providers })
  • Xotiradagi router'ning boshlang'ich manzili: <MemoryRouter ={["/menyu"]}>
  • Testda xatoni darhol ko'rish: queries: { : false }
  • Hookni komponentsiz sinash: const { result } = (() => useCart())
  • Zustand store'ni tiklash: useOrder.setState(useOrder.(), true)
Yechim
  • wrapper — rerender da ham saqlanadi.
  • initialEntries — massiv: tarix yozuvlari, oxirgisi joriy.
  • retry — sukut 3; testda false.
  • renderHook — natija result.current da.
  • getInitialState — create() dagi holat; true — almashtirish.

2-mashq (o'rta): savat sahifasi

CartPage ikki narsaga tayanadi: useCart (Zustand store: items, add, clear) va useNavigate. Savat bo'sh bo'lsa — «Savat bo'sh». Aks holda — ro'yxat va «Rasmiylashtirish» tugmasi, u /tolov ga o'tadi. renderWithRouter(ui, { route }) yordamchisini yozing, store tiklash uning ichida bo'lsin. Uch test: (1) bo'sh savat; (2) store'ga ikki taom qo'shib — ro'yxatda 2 ta; (3) tugma bosilganda Routes dagi /tolov sahifasi («To'lov» sarlavhasi). Testlar har qanday tartibda o'tsin.

Ishora: store'ni render'dan keyin act(() => useCart.getState().add("Osh")) bilan to'ldiring.

Yechim
jsx
import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
  MemoryRouter,
  Route,
  Routes,
  useNavigate,
} from "react-router";
import { expect, test } from "vitest";
import { create } from "zustand";

const useCart = create((set) => ({
  items: [],
  add: (d) => set((s) => ({ items: [...s.items, d] })),
  clear: () => set({ items: [] }),
}));

function CartPage() {
  const items = useCart((s) => s.items);
  const navigate = useNavigate();
  if (items.length === 0) return <p>Savat bo'sh</p>;
  return (
    <section aria-label="Savat">
      <ul>
        {items.map((d) => <li key={d}>{d}</li>)}
      </ul>
      <button onClick={() => navigate("/tolov")}>
        Rasmiylashtirish
      </button>
    </section>
  );
}

function renderWithRouter(ui, { route = "/savat" } = {}) {
  useCart.setState(useCart.getInitialState(), true);
  return render(
    <MemoryRouter initialEntries={[route]}>
      <Routes>
        <Route path="/savat" element={ui} />
        <Route path="/tolov" element={<h1>To'lov</h1>} />
      </Routes>
    </MemoryRouter>,
  );
}

test("bo'sh savat", () => {
  renderWithRouter(<CartPage />);
  expect(screen.getByText("Savat bo'sh")).toBeInTheDocument();
});

test("ikki taom", () => {
  renderWithRouter(<CartPage />);
  act(() => {
    useCart.getState().add("Osh");
    useCart.getState().add("Manti");
  });
  console.log(screen.getAllByRole("listitem").length);
  expect(screen.queryByText("Savat bo'sh")).toBeNull();
});

test("rasmiylashtirish — to'lov sahifasi", async () => {
  const user = userEvent.setup();
  renderWithRouter(<CartPage />);
  act(() => useCart.getState().add("Osh"));
  await user.click(
    screen.getByRole("button", { name: "Rasmiylashtirish" }),
  );
  expect(
    screen.getByRole("heading", { name: "To'lov" }),
  ).toBeInTheDocument();
});

Konsolda:

text
  ✓ bo'sh savat
2
  ✓ ikki taom
  ✓ rasmiylashtirish — to'lov sahifasi
Testlar: 3 o'tdi (jami 3)

Store tiklash renderWithRouter ichida — test qaysi tartibda yursa ham, savat bo'sh boshlanadi. Store render'dan keyin o'zgargani uchun act kerak: yangilanish RTL'dan tashqarida (RTL I).

3-mashq (qiyin): yangilanadigan narx

usePrice(dish) hooki useQuery bilan ["price", dish] kalitida narxni oladi (loadPrice parametr bilan). Testlar: (1) renderHook + Query o'rami. dish «Osh» dan «Manti» ga o'zgarsa (rerender), yangi narx keladi. loadPrice ikki marta chaqiriladi — har kalitga bir marta. (2) «Osh» ga qaytilganda kesh ishlaydi — loadPrice uchinchi marta chaqirilmaydi (staleTime: Infinity); (3) har test o'z client'i bilan — ikkinchi test birinchisining keshini ko'rmasligini isbotlang.

Ishora: initialProps: { dish: "Osh" }; waitFor(() => expect(result.current.data).toBe(…)).

Yechim
jsx
import { renderHook, waitFor } from "@testing-library/react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";
import { expect, test, vi } from "vitest";

const PRICES = { Osh: 35000, Manti: 30000 };

function usePrice(dish, loadPrice) {
  return useQuery({
    queryKey: ["price", dish],
    queryFn: () => loadPrice(dish),
    staleTime: Infinity,
  });
}

function renderPrice(loadPrice) {
  const client = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  const wrapper = ({ children }) => (
    <QueryClientProvider client={client}>
      {children}
    </QueryClientProvider>
  );
  return renderHook(({ dish }) => usePrice(dish, loadPrice), {
    wrapper,
    initialProps: { dish: "Osh" },
  });
}

test("taom o'zgarsa — yangi narx, keyin kesh", async () => {
  const loadPrice = vi.fn(async (dish) => PRICES[dish]);
  const { result, rerender } = renderPrice(loadPrice);
  await waitFor(() => expect(result.current.data).toBe(35000));
  rerender({ dish: "Manti" });
  await waitFor(() => expect(result.current.data).toBe(30000));
  rerender({ dish: "Osh" });
  expect(result.current.data).toBe(35000);
  console.log(loadPrice.mock.calls.map((c) => c[0]));
});

test("yangi test — yangi kesh", async () => {
  const loadPrice = vi.fn(async (dish) => PRICES[dish]);
  const { result } = renderPrice(loadPrice);
  expect(result.current.isPending).toBe(true);
  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(loadPrice).toHaveBeenCalledTimes(1);
});

Konsolda:

text
(2) ['Osh', 'Manti']
  ✓ taom o'zgarsa — yangi narx, keyin kesh
  ✓ yangi test — yangi kesh
Testlar: 2 o'tdi (jami 2)

«Osh» ga qaytganda natija darhol (waitFor siz) keldi — keshdan; loadPrice faqat ikki kalit uchun chaqirildi. Ikkinchi test isPending: true bilan boshlandi: kesh bo'sh, demak client yangi.

4-mashq: Vazifalar qadami

test/chiz-yordamchisi branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: tekshiruv/yordamchi/chiz.tsx, providerli-hooklar.test.tsx (6), App/harakatlar/qoralama testlarini chiz ga o'tkazish.

Yechim

Tekshiruv ro'yxati:

  • Avval Sozlama maydonlarini | undefined siz yozing — npm run tip dagi TS2379 ni o'zingiz ko'ring, keyin tuzating.
  • yangiMijoz va storelarniTikla ni export bilan qoldiring — npm run knip "Unused exports (2)" deydi.
  • chiz dagi storelarniTikla() qatorini vaqtincha o'chiring — providerli hooklar faylining 2-testi yiqiladi. Qaytaring.
  • npm test — 343 test; App.test.tsx — 215 qator.

12. Real ishda

  • Har loyihada test-utils bor: Redux hujjatida renderWithProviders, TanStack Query hujjatida createWrapper, Testing Library hujjatida "custom render" — nomi turlicha, g'oyasi bitta.
  • Yangi provider ilovaga qo'shilsa, birinchi bo'lib custom render'ga qo'shiladi. Kod ko'rib chiqishda: "test o'ramiga qo'shdingmi?"
  • Tartibga bog'liq testlar CI'da ko'rinadi: Vitest --sequence.shuffle bilan testlarni aralash tartibda yurgizadi — oqish bo'lsa, darhol yiqiladi.
  • Intervyu savollari: "custom render nima uchun?", "nega har testga yangi QueryClient?", "renderHook va komponent testi farqi?", "Zustand store'ni testlar orasida qanday tiklaysiz?".

Xulosa

  • Provider'ga tayangan komponent yolg'iz chizilmaydi; wrapper — o'ram, custom render — o'ramning bitta joydagi yordamchisi.
  • Har chaqiruvda yangi QueryClient (retry: false); client o'ramdan tashqarida yaratiladi va qaytariladi.
  • Router: MemoryRouter initialEntries (sukut), URL'ni tekshirishda BrowserRouter + history tiklash, loader'li sahifa — createMemoryRouter.
  • Global holat (til, store, localStorage, history) har testdan oldin joyiga qaytadi; Zustand — setState(getInitialState(), true).
  • renderHook — result.current (har safar qaytadan o'qing), rerender, wrapper; natijani waitFor bilan kutish.
  • vazifalar: chiz() va hookniChiz(), 343 test; TS2379 va knip — test kodi ham kod.

Keyingi dars: MSW: API'ni tarmoq darajasida mock qilish — har faylda qo'lda yozilgan fetch soxtalari o'rniga bitta tarmoq darajasidagi soxta server.

Manbalar

  • Testing Library: Setup — "Custom Render" (testing-library.com/docs/react-testing-library/setup)
  • Testing Library API: render (wrapper), renderHook (initialProps, rerender)
  • TanStack Query v5: Testing — "turn off retries", har testga yangi QueryClient (tanstack.com/query/latest/docs/framework/react/guides/testing)
  • React Router 8: createMemoryRouter, MemoryRouter (reactrouter.com)
  • Zustand: Testing — store'larni tiklash, getInitialState (zustand.docs.pmnd.rs/guides/testing)
  • Haqiqiy chiqishlar: kanon vazifalar holat 06 (Vitest 5.0.4, Node 24.21.0, 2026-10-19); natija oynasi — Chrome 155
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Testing Library III: custom render, providerlar, router va renderHook — IlmHamroh