19-bo‘lim
Testlar
React Testing Library bilan sinash, foydalanuvchi ko'zi bilan qidirish, hodisalarni yuborish, asinxron kutish va nimani sinash kerakligi.
Ushbu bo‘lim mundarijasi
React komponentini sinashning ikki yo'li bor: ichki tuzilishini tekshirish yoki foydalanuvchi ko'radigan narsani tekshirish.
Ikkinchisi ancha foydali - va React Testing Library aynan shu uchun yozilgan.
O'rnatish #
npm install --save-dev @testing-library/react @testing-library/dom jsdom
Test yurituvchi sifatida Node ning o'zinikini ishlatamiz -
Node.js darsligining 19-bo'limida ko'rgan node --test.
Birinchi test #
import { JSDOM } from "jsdom";
const dom = new JSDOM("<!doctype html><html><body></body></html>");
globalThis.window = dom.window;
globalThis.document = dom.window.document;
Object.defineProperty(globalThis, "navigator", {
value: dom.window.navigator,
configurable: true,
});
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
for (const nom of ["HTMLElement", "Node", "Event", "MouseEvent",
"getComputedStyle"]) {
globalThis[nom] = dom.window[nom];
}
Bu fayl brauzer muhitini tayyorlaydi. Vite loyihasida odatda
vitest ishlatiladi va u buni o'zi qiladi.
import { useState } from "react";
export function Kartochka({ ism, ball }) {
const [ochiq, ozgartir] = useState(false);
return (
<article>
<h3>{ism}</h3>
<button onClick={() => ozgartir(!ochiq)}>
{ochiq ? "Yopish" : "Batafsil"}
</button>
{ochiq && <p>Ball: {ball}</p>}
</article>
);
}
import { run } from "node:test";
import { writeFile, rm } from "node:fs/promises";
await writeFile("kartochka.test.mjs", `
import "./sinov-muhiti.mjs";
import { test, afterEach } from "node:test";
import assert from "node:assert/strict";
import { render, screen, cleanup } from "@testing-library/react";
import { Kartochka } from "./kartochka.mjs";
// Har bir testdan keyin chizilganni tozalaymiz - aks holda
// keyingi test oldingisining elementlarini ham ko'radi.
afterEach(cleanup);
test("ismni ko'rsatadi", () => {
render(<Kartochka ism="Malika" ball={88} />);
assert.ok(screen.getByText("Malika"));
});
test("boshida ball yashirin", () => {
render(<Kartochka ism="Malika" ball={88} />);
assert.equal(screen.queryByText(/Ball:/), null);
});
test("tugmada Batafsil yozilgan", () => {
render(<Kartochka ism="Malika" ball={88} />);
assert.equal(screen.getByRole("button").textContent, "Batafsil");
});
`);
// JSX ni o'girish uchun test faylini ham esbuild dan o'tkazamiz
const { execFileSync } = await import("node:child_process");
execFileSync("./node_modules/.bin/esbuild", [
"kartochka.test.mjs", "--loader:.mjs=jsx", "--jsx=automatic",
"--format=esm", "--log-level=error",
"--outfile=kartochka.tayyor.test.mjs",
]);
let otdi = 0;
let yiqildi = 0;
const nomlar = [];
for await (const h of run({ files: ["kartochka.tayyor.test.mjs"] })) {
if (h.type === "test:pass") { otdi++; nomlar.push(h.data.name); }
if (h.type === "test:fail") {
yiqildi++;
nomlar.push(`${h.data.name} (YIQILDI)`);
}
}
console.log(nomlar.join("\n"));
console.log(`O'tdi: ${otdi} | Yiqildi: ${yiqildi}`);
await rm("kartochka.test.mjs");
await rm("kartochka.tayyor.test.mjs");
ismni ko'rsatadi
boshida ball yashirin
tugmada Batafsil yozilgan
O'tdi: 3 | Yiqildi: 0
Uchta test ham o'tdi.
render komponentni chizadi, screen esa unda qidirish
imkonini beradi.
Qidirish usullari #
| Usul | Topilmasa |
|---|---|
getBy... | Xato tashlaydi |
queryBy... | null qaytaradi |
findBy... | Kutadi, keyin xato tashlaydi |
Qoida oddiy:
- Bor bo'lishi kerak →
getBy.... Topilmasa test yiqiladi va xato xabari tushunarli bo'ladi. - Yo'q bo'lishi kerak →
queryBy.... Faqat shu holatda, chunki unullqaytaradi. - Keyinroq paydo bo'ladi →
findBy.... U asinxron vaawaitbilan ishlatiladi.
queryBy ni "bor" tekshiruvida ishlatmang: u null qaytaradi
va assert.ok(null) xatosi nima yo'qolganini aytmaydi.
Foydalanuvchi ko'zi bilan qidirish #
import "./sinov-muhiti.mjs";
import { render, screen } from "@testing-library/react";
function Forma() {
return (
<form>
<label htmlFor="ism">To'liq ism</label>
<input id="ism" />
<button type="submit">Yuborish</button>
<img src="/logo.png" alt="Kompaniya belgisi" />
</form>
);
}
render(<Forma />);
console.log("rol bo'yicha:", screen.getByRole("button").textContent);
console.log("yorliq bo'yicha:", screen.getByLabelText("To'liq ism").id);
console.log("alt bo'yicha:",
screen.getByAltText("Kompaniya belgisi").tagName);
console.log("matn bo'yicha:", screen.getByText("To'liq ism").tagName);
rol bo'yicha: Yuborish
yorliq bo'yicha: ism
alt bo'yicha: IMG
matn bo'yicha: LABEL
| Qidiruv | Nima bo'yicha |
|---|---|
getByRole | Elementning roli (button, heading) |
getByLabelText | Forma yorlig'i |
getByText | Ko'rinadigan matn |
getByAltText | Rasm tavsifi |
getByTestId | Oxirgi chora |
Eski testlarda ko'p uchraydi:
container.querySelector(".tugma-asosiy > span");
Bu mo'rt: dizayner sinf nomini o'zgartirsa yoki <span>
o'rniga <b> qo'ysa, test yiqiladi - garchi ilova mukammal
ishlayotgan bo'lsa ham.
getByRole("button") esa foydalanuvchi ko'radigan narsani
tekshiradi. Sinf ham, teg ham, joylashuv ham o'zgarishi mumkin -
test baribir ishlaydi.
Qo'shimcha foyda: getByRole va getByLabelText ishlashi uchun
belgilash to'g'ri bo'lishi kerak - <label> bog'langan,
tugma haqiqiy <button>. Ya'ni bunday testlar bilvosita
kirish imkoniyatini ham tekshiradi.
getByTestId ni faqat boshqa yo'l qolmaganda ishlating.
Hodisalarni yuborish #
import { run } from "node:test";
import { writeFile, rm } from "node:fs/promises";
import { execFileSync } from "node:child_process";
await writeFile("hodisa.test.mjs", `
import "./sinov-muhiti.mjs";
import { test, afterEach } from "node:test";
import assert from "node:assert/strict";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { Kartochka } from "./kartochka.mjs";
afterEach(cleanup);
test("tugma bosilganda ball ko'rinadi", () => {
render(<Kartochka ism="Malika" ball={88} />);
assert.equal(screen.queryByText("Ball: 88"), null);
fireEvent.click(screen.getByRole("button"));
assert.ok(screen.getByText("Ball: 88"));
assert.equal(screen.getByRole("button").textContent, "Yopish");
});
test("ikkinchi bosishda yana yashirinadi", () => {
render(<Kartochka ism="Malika" ball={88} />);
const tugma = screen.getByRole("button");
fireEvent.click(tugma);
fireEvent.click(tugma);
assert.equal(screen.queryByText("Ball: 88"), null);
});
`);
execFileSync("./node_modules/.bin/esbuild", [
"hodisa.test.mjs", "--loader:.mjs=jsx", "--jsx=automatic",
"--format=esm", "--log-level=error",
"--outfile=hodisa.tayyor.test.mjs",
]);
let otdi = 0;
let yiqildi = 0;
for await (const h of run({ files: ["hodisa.tayyor.test.mjs"] })) {
if (h.type === "test:pass") otdi++;
if (h.type === "test:fail") yiqildi++;
}
console.log(`O'tdi: ${otdi} | Yiqildi: ${yiqildi}`);
await rm("hodisa.test.mjs");
await rm("hodisa.tayyor.test.mjs");
O'tdi: 2 | Yiqildi: 0
fireEvent.click(...) hodisani yuboradi va React ning
yangilanishini kutadi.
Test foydalanuvchi qiladigan ishni takrorlaydi: tugmani bosadi va natijani ko'radi. Komponentning ichki holatiga umuman tegmaydi.
Asinxron kutish #
import { run } from "node:test";
import { writeFile, rm } from "node:fs/promises";
import { execFileSync } from "node:child_process";
await writeFile("kutish.test.mjs", `
import "./sinov-muhiti.mjs";
import { test, afterEach } from "node:test";
import assert from "node:assert/strict";
import { render, screen, cleanup } from "@testing-library/react";
import { useState, useEffect } from "react";
afterEach(cleanup);
function Kechikkan() {
const [matn, ozgartir] = useState("yuklanmoqda");
useEffect(() => {
const id = setTimeout(() => ozgartir("tayyor"), 20);
return () => clearTimeout(id);
}, []);
return <p>{matn}</p>;
}
test("kechikkan mazmunni kutadi", async () => {
render(<Kechikkan />);
assert.ok(screen.getByText("yuklanmoqda"));
const tayyor = await screen.findByText("tayyor");
assert.ok(tayyor);
});
`);
execFileSync("./node_modules/.bin/esbuild", [
"kutish.test.mjs", "--loader:.mjs=jsx", "--jsx=automatic",
"--format=esm", "--log-level=error",
"--outfile=kutish.tayyor.test.mjs",
]);
let otdi = 0;
let yiqildi = 0;
for await (const h of run({ files: ["kutish.tayyor.test.mjs"] })) {
if (h.type === "test:pass") otdi++;
if (h.type === "test:fail") yiqildi++;
}
console.log(`O'tdi: ${otdi} | Yiqildi: ${yiqildi}`);
await rm("kutish.test.mjs");
await rm("kutish.tayyor.test.mjs");
O'tdi: 1 | Yiqildi: 0
findByText element paydo bo'lguncha kutadi - aniq vaqtni
yozish kerak emas.
Test yozganda vasvasa paydo bo'ladi:
await new Promise((y) => setTimeout(y, 500));
Bu yomon ikki sababga ko'ra:
- Sekin - har bir test yarim soniya kutadi;
- Ishonchsiz - sekin kompyuterda 500 ms yetmasligi mumkin va test tasodifan yiqiladi.
Bunday testlarni "qaltiroq" (flaky) deb atashadi va ular jamoaning eng katta bezovtaligi: ular goh o'tadi, goh yiqiladi va hech kim ularga ishonmay qo'yadi.
findBy... va waitFor esa shartni kutadi: natija tayyor
bo'lishi bilan davom etadi.
Nimani sinash kerak #
import "./sinov-muhiti.mjs";
import { render, screen, cleanup } from "@testing-library/react";
function Chegirma({ narx, foiz }) {
const chegirma = Math.round(narx * (foiz / 100));
const yakuniy = narx - chegirma;
return (
<div>
<p>Narx: {narx}</p>
{foiz > 0 && <p>Chegirma: {chegirma}</p>}
<p>To'lov: {yakuniy}</p>
</div>
);
}
render(<Chegirma narx={100000} foiz={20} />);
console.log(screen.getByText(/To'lov/).textContent);
console.log(screen.getByText(/Chegirma/).textContent);
cleanup();
render(<Chegirma narx={100000} foiz={0} />);
console.log("chegirmasiz:", screen.queryAllByText(/Chegirma/).length);
To'lov: 80000
Chegirma: 20000
chegirmasiz: 0
Yuqoridagi test fayllarida afterEach(cleanup) bor.
render komponentni document.body ga qo'shadi va uni o'zi
olib tashlamaydi. Tozalanmasa, ikkinchi test birinchisining
elementlarini ham ko'radi va getByRole("button") bir nechta
tugma topib xato tashlaydi:
Found multiple elements with the role "button"
vitest yoki jest ishlatsangiz, RTL buni avtomatik
qiladi. node --test bilan esa o'zingiz yozasiz.
Xuddi shu sabab misol ichida ikki marta chizganda ham
cleanup() chaqirilgan.
Yaxshi test nima bo'lishi kerakligini tekshiradi:
- To'g'ri matn ko'rinadimi;
- Tugma bosilganda kerakli narsa sodir bo'ladimi;
- Chegaraviy holat to'g'ri ishlaydimi (nol, bo'sh, xato).
Yomon test qanday qilinganini tekshiradi:
- Qaysi holat o'zgaruvchisi bor;
- Nechta marta qayta chizildi;
- Qanday sinf nomi ishlatilgan.
Farqning amaliy ma'nosi: yaxshi testlar kodni qayta yozganda
ham ishlaydi. Komponentni useState dan useReducer ga
o'tkazsangiz, xatti-harakat o'zgarmaydi - demak testlar ham
o'zgarmaydi.
Bu testlarning asosiy foydasi: ular o'zgarishdan qo'rqmaslik imkonini beradi.
Nimani sinamaslik kerak #
| Sinamang | Sabab |
|---|---|
| React ning o'zi | U allaqachon sinalgan |
| Uchinchi tomon kutubxonalari | Ularning o'z testlari bor |
| Aniq sinf nomlari | Dizayn o'zgaradi |
| Ichki holat nomlari | Amalga oshirish tafsiloti |
| Har bir kichik komponent | Vaqtni yeydi, foydasi kam |
Bu bobda node --test ishlatildi, chunki u Node bilan birga
keladi va Node.js darsligida uni ko'rgansiz.
Vite loyihalarida esa odatda Vitest olinadi:
npm install --save-dev vitest @testing-library/react jsdom
Uning afzalligi: Vite sozlamalarini qayta ishlatadi - JSX,
yo'l taxalluslari, muhit o'zgaruvchilari o'zi ishlaydi. Yuqorida
qo'lda yozgan sinov-muhiti.mjs kerak bo'lmaydi.
Sintaksis deyarli bir xil: test, expect, describe. Shuning
uchun bu bobdagi bilim to'g'ridan-to'g'ri o'tadi.
- Komponentni
renderqilib, matnni toping. getByRolebilan tugmani toping.queryBybilan yo'qligini tekshiring.getByni yo'q element uchun ishlatib, xatoni o'qing.fireEvent.clickbilan tugmani bosing.- Ikki marta bosib, holat qaytganini tekshiring.
findByTextbilan kechikkan mazmunni kuting.setTimeoutbilan kutib ko'ring va farqni o'ylang.- Sinf nomi bo'yicha test yozib, keyin uni
getByRolega o'tkazing. - Chegaraviy holat (nol, bo'sh) uchun test qo'shing.
Xulosa #
- React Testing Library foydalanuvchi ko'radigan narsani sinaydi.
getBy- bor bo'lishi kerak,queryBy- yo'qligini tekshirish,findBy- kutish.getByRolevagetByLabelTextafzal: ular sinf va tuzilmaga bog'liq emas va kirish imkoniyatini ham tekshiradi.fireEventfoydalanuvchi harakatini takrorlaydi.- Kutish uchun
findBy,setTimeoutemas - aks holda test qaltiroq bo'ladi. - Xatti-harakatni sinang, amalga oshirishni emas.
- Vite loyihalarida odatda Vitest ishlatiladi; sintaksis deyarli bir xil.
Keyingi bo'limda hammasini birlashtiramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.