19-bo‘lim

Testlar

React Testing Library bilan sinash, foydalanuvchi ko'zi bilan qidirish, hodisalarni yuborish, asinxron kutish va nimani sinash kerakligi.

🕑 12 daqiqa o‘qish 📄 821 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. O'rnatish
  2. Birinchi test
  3. Qidirish usullari
  4. Foydalanuvchi ko'zi bilan qidirish
  5. Hodisalarni yuborish
  6. Asinxron kutish
  7. Nimani sinash kerak
  8. Nimani sinamaslik kerak
  9. Xulosa

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 #

Terminal
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 #

JSX
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.

JSX
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>
    );
}
JSX
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");
Natija
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 #

UsulTopilmasa
getBy...Xato tashlaydi
queryBy...null qaytaradi
findBy...Kutadi, keyin xato tashlaydi
Qaysi qidiruvni tanlash

Qoida oddiy:

  • Bor bo'lishi kerakgetBy.... Topilmasa test yiqiladi va xato xabari tushunarli bo'ladi.
  • Yo'q bo'lishi kerakqueryBy.... Faqat shu holatda, chunki u null qaytaradi.
  • Keyinroq paydo bo'ladifindBy.... U asinxron va await bilan ishlatiladi.

queryBy ni "bor" tekshiruvida ishlatmang: u null qaytaradi va assert.ok(null) xatosi nima yo'qolganini aytmaydi.

Foydalanuvchi ko'zi bilan qidirish #

JSX
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);
Natija
rol bo'yicha: Yuborish
yorliq bo'yicha: ism
alt bo'yicha: IMG
matn bo'yicha: LABEL
QidiruvNima bo'yicha
getByRoleElementning roli (button, heading)
getByLabelTextForma yorlig'i
getByTextKo'rinadigan matn
getByAltTextRasm tavsifi
getByTestIdOxirgi chora
Sinf va tuzilma bo'yicha qidirmang

Eski testlarda ko'p uchraydi:

JavaScript
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 #

JSX
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");
Natija
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 #

JSX
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");
Natija
O'tdi: 1 | Yiqildi: 0

findByText element paydo bo'lguncha kutadi - aniq vaqtni yozish kerak emas.

setTimeout bilan kutmang

Test yozganda vasvasa paydo bo'ladi:

JavaScript
await new Promise((y) => setTimeout(y, 500));

Bu yomon ikki sababga ko'ra:

  1. Sekin - har bir test yarim soniya kutadi;
  2. 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 #

JSX
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);
Natija
To'lov: 80000
Chegirma: 20000
chegirmasiz: 0
cleanup ni unutmang

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:

Natija
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.

Xatti-harakatni sinang, amalga oshirishni emas

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 #

SinamangSabab
React ning o'ziU allaqachon sinalgan
Uchinchi tomon kutubxonalariUlarning o'z testlari bor
Aniq sinf nomlariDizayn o'zgaradi
Ichki holat nomlariAmalga oshirish tafsiloti
Har bir kichik komponentVaqtni yeydi, foydasi kam
Vitest - amaldagi odatiy tanlov

Bu bobda node --test ishlatildi, chunki u Node bilan birga keladi va Node.js darsligida uni ko'rgansiz.

Vite loyihalarida esa odatda Vitest olinadi:

Terminal
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.

Amaliy topshiriq
  1. Komponentni render qilib, matnni toping.
  2. getByRole bilan tugmani toping.
  3. queryBy bilan yo'qligini tekshiring.
  4. getBy ni yo'q element uchun ishlatib, xatoni o'qing.
  5. fireEvent.click bilan tugmani bosing.
  6. Ikki marta bosib, holat qaytganini tekshiring.
  7. findByText bilan kechikkan mazmunni kuting.
  8. setTimeout bilan kutib ko'ring va farqni o'ylang.
  9. Sinf nomi bo'yicha test yozib, keyin uni getByRole ga o'tkazing.
  10. 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.
  • getByRole va getByLabelText afzal: ular sinf va tuzilmaga bog'liq emas va kirish imkoniyatini ham tekshiradi.
  • fireEvent foydalanuvchi harakatini takrorlaydi.
  • Kutish uchun findBy, setTimeout emas - 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.

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.