Jak začít psát jednotkové testy pro React with jest and enzyme

Poprvé zveřejněno na js.dev → https://thejs.dev/jmitchell/how-to-get-started-writing-unit-tests-for-react-with-enzyme-and-jest-mfw

Testování v Reactu může být pro vývojáře často výzvou. Na výběr je spousta možností a knihoven, některé se používají obtížněji než jiné. Něco z toho je způsobeno složitostí nebo syntaxí testovacích rámců, zatímco React zavádí spoustu vlastních problémů, se kterými může být obtížné se vypořádat, jako je testování háčků a odložené akce.

Tento výukový článek předpokládá, že máte základní znalosti o metodologii testování, ať už pomocí knihoven jest, jasmine nebo jiných knihoven, a jste obeznámeni s describe , it , before* , after* syntaxe.

Reagovat a žertovat

Jest se snapshops je React doporučeným způsobem práce, ale také doporučují, abyste se podívali na vyzkoušení testovací knihovny React. Existuje starý způsob testování, pre-jest, který spočívá ve vyvolání událostí na prvcích zabalených do act() a pak prosazování změn dom. Tento přístup nebudeme v tomto příspěvku obhajovat.

Testovací knihovny JavaScriptu jasmine, chai, sinon a mocha formovaly způsob, jakým vývojáři píší své testy, se snadno pochopitelnou syntaxí, tvrditelnými stavy, zesměšňováním, špiony a dalšími. Jest a Enzyme jsou v testovacím světě relativními nováčky, ale přinášejí stejné nápady, které tak dobře fungovaly v minulosti, a zároveň představují několik vlastních nových nápadů.

Jest je testovací nástroj JavaScript vytvořený pro JavaScript společností Facebook, primárně pro podporu testovacího úsilí v jejich rámci React. Využívá koncepty zavedené z knihoven chai, sinon, mocha, jasmine a dalších, dokonce je používá pod nimi k podpoře tvrzení, zesměšňování a špehování, k vytvoření testovacího prostředí přívětivého pro vývojáře. Díky svému zjednodušenému přístupu k testování a velmi snadné integraci do Reactu se stala preferovanou testovací knihovnou pro aplikace React.

Kam se hodí enzym

Ne každá testovací knihovna je však vytvořena stejně a v aplikacích React je značná složitost, která je příliš obtížné snadno testovat pomocí jest. To je místo, kde Enzyme překlenuje mezeru, jako alternativní nástroj pro testování JavaScriptu pro React, vyvinutý společností Airbnb. Knihovna si klade za cíl usnadnit testování vašich komponent tím, že umožňuje vývojářům manipulovat, procházet a simulovat běhové události a pracovat s výsledky. V praxi to funguje lépe než na papíře.

Enzyme má aktuálně (v době psaní tohoto článku) adaptéry, které budou fungovat s verzemi React 16.x , 15.x , 0.14.x a 0.13.x . Jediné, co musí uživatel udělat, je nainstalovat plugin pomocí svého preferovaného správce balíčků a nakonfigurovat adaptér v běhovém prostředí Enzyme.

Rozdíl mezi react-testing-library a enzyme je, že enzym není navržen tak, aby potlačoval testovací prostředí, je to nástroj, který můžete použít vedle žertu. Testovací knihovna React může sloužit stejnému účelu, ale také poskytuje funkce pro zásadní změnu testovacího běhového prostředí.

Začínáme

Kód:Krok 1 – Přidání vtipu.

V tomto tutoriálu budeme používat úložiště, které jsem vytvořil, se základním prvkem vstupního formuláře komponenty a vykresleným prvkem pro výstup. Jak budeme postupovat tímto příspěvkem, budu sdílet odkazy s více dokončeným kódem. Pojďme se tedy podívat na kód a rozlousknout se!

Toto je náš základní formulář, který budeme testovat:

Instalace enzymu

Kód:Krok 2 – Instalace a konfigurace enzymu.

Proces instalace enzymu je přímočarý a má následující kroky:

  • Nainstalujte knihovnu prostřednictvím preferovaného správce balíčků.
  • Nakonfigurujte enzymový adaptér
  • Nakonfigurujte jest tak, aby používal enzym
  • Otestujte!

Toto je aktuální adaptér pro reakci na mapování verzí. V závislosti na verzi React, kterou váš projekt používá, budete muset nainstalovat jeden z těchto adaptérů (úplný seznam adaptérů a kompatibility).

Balík adaptéru Reagovat semver verzi
enzyme-adapter-react-16 ^16.4.0-0
enzyme-adapter-react-16.3 ~16.3.0-0
enzyme-adapter-react-16.2 ~16.2

Abychom mohli začít s enzymem, jednoduše jej nainstalujeme do našeho projektu spolu s požadovaným adaptérem:

npm i --save-dev enzyme enzyme-adapter-react-16
Konfigurace adaptéru

Jakmile je enzym nainstalován, musíme nakonfigurovat enzym tak, aby používal adaptér, který chcete použít pro verzi React, kterou máte. K tomu můžete použít Enzyme.Configure() nejvyšší úrovně API:

// enzyme.setup.js
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });
Přidávání do žertu

Abychom mohli používat enzymové testy s žertem, musíme provést další krok instalace a konfigurace, přidání jest-enzymu a jeho konfiguraci pomocí žertu.

npm i --save-dev jest-enzyme

Pokud ještě nemáte jest.config.js vytvořte jej zkopírováním jest.config.js v úložišti.

Potřebujeme přidat konfigurační skript enzymu do jest.config.js soubor:

module.exports = {
  ...
  setupFiles: [ "<rootDir>/enzyme.setup.js" ]
}

Nyní, když jsme přidali enzym, pojďme se pustit do testů!

Havarijní kurz enzymu

Jednou z hlavních výhod enzymu je více než jest a react-testing-library je způsob, jakým můžete testovat vykreslování a interaktivitu komponent pomocí pomocných metod shallow , mount a render .

Mělké vykreslování s shallow()

Mělké vykreslování je extrémně užitečný nástroj, protože vás nutí testovat komponentu jako celek, aniž byste se museli obávat nechtěného testování a prosazování chování podřízených prvků, háčků a dalších funkcí.

shallow API volá metody životního cyklu React, jako je componentDidMount a componentDidUpdate .

Úplné vykreslování DOM s mount()

Montáž komponenty je ekvivalentní render() v knihovně reakčních testů; provede úplné vykreslení dom. To je ideální pro případy použití, kdy máte komponenty, které mohou interagovat s API, mají odložené akce, interagují s háčky nebo jsou zabaleny do komponent vyššího řádu.

Na rozdíl od mělkého nebo statického vykreslování mount ve skutečnosti připojí komponentu do DOM, což znamená, že testy se mohou navzájem ovlivňovat, pokud používají stejný DOM.

Statické vykreslování komponenty s render()

Statické vykreslování využívá render() funkce pro generování HTML z vašeho stromu React, pro porovnání a analýzu výsledků. Vrácená obálka je podobná mount a shallow , nicméně render používá knihovnu třetí strany cheerio provést procházení a analýzu.

Podpora háčků React

Enzyme podporuje háky React s určitými omezeními v shallow , kvůli React rendereru. Následující háčky není chovat se podle očekávání při použití shallow :

  • useEffect() a useLayoutEffect() nezavolá
  • useCallback() neukládá zpětné volání do paměti

Psaní enzymových testů

Kód:Krok 3 – Psaní enzymových testů.

Struktura naší aplikace se skládá z jediné komponenty Name , s testovacím souborem name.test.js . První test, který budeme psát, bude využívat enzymu shallow() renderer, který umí pracovat s háčky jako useReducer() a useState() , které jsou životně důležité pro funkční komponenty.

Píšeme náš první test

V tomto testu potvrdíme, že se komponenta vykresluje s prázdným názvem přidáním testů do souboru Testovací sady Name:<root>/src/components/name/name.test.js .

V tomto souboru již existují testy, takže je můžeme smazat, takže nám zůstane jeden describe :

describe("Component: Name", () => {
});

Nejprve musíme nastavit přípravky v naší nové testovací sadě, abychom pro každý test napsali méně kotelního štítku. Do Component: Name přidejte následující testovací sada:

let component = null;

beforeEach(() => {
  component = shallow(<Name />);
})

afterEach(() => {
  component = null;
});

Nyní, když máme naše zařízení nastaveno, můžeme potvrdit, že se komponenta mělká vykresluje správně pomocí exists() užitková funkce:

it("should render component", () => {
  expect(component.exists("form")).toBe(true);
})

Mělké rozhraní API nám poskytuje užitečné pomocné funkce, které nám umožňují dotazovat se na renderovaný DOM způsobem podobným jQuery. Můžeme se dotazovat na přímé odkazy na komponentu, id, třídu, prvek a ještě složitější řetězce dotazů.

shallow také poskytuje další funkce v ShallowWrapper API pro kontrolu prvků na pozicích uzlů, pokud existují, zda jsou viditelné a další.

Prosazování výchozích stavů

Vzhledem k určitým vstupům je vždy rozumné uplatňovat výchozí vykreslený stav komponenty. Jsou chvíle, kdy se komponenta může nacházet ve stavu toku díky asynchronním a odloženým akcím, jako je useEffect háček, ale stále potřebujeme otestovat tyto počáteční, neurčité a konečné stavy.

Pojďme přidat další test a ověřit, že naše Name komponenta se vykreslí podle očekávání bez stavu s text() užitková funkce.

it("should render default state", () => {
  expect(component.exists("#output")).toBe(true);
  expect(component.find("#output").text()).toBe("Hello, ");
});

Po zadání se zobrazí název tvrzení

Nyní je čas simulovat interakce uživatelů a odraz těchto interakcí v našem uživatelském rozhraní. Enzyme poskytuje užitečnou užitečnou funkci s shallow renderer, abyste toho dosáhli pomocí simulate() .

Simulovat umožňuje uživateli spustit událost na aktuálním uzlu. Přidejme náš test, abychom se ujistili, že se při zadání zobrazí naše jméno.

it("should display name when user types into input field", () => {
  component.find("input#name").simulate("change", { target: { value: "Charles" } });
  expect(component.find("#output").text()).toBe("Hello, Charles");
});

Mělké vykreslování s podřízenými komponentami

Kód:Fáze 4 – Mělké vykreslování s podřízenými uzly

Myšlenka mělkého vykreslování je vykreslení pouze komponenty, kterou potřebujete, ale jsou chvíle, kdy to prostě není možné, například když jsou komponenty závislé na sdílených knihovnách nebo sdílených zdrojích. Jindy možná budete muset ověřit, jak se komponenta chová, když je použit jiný motiv nebo jazyk, a tyto změny se obvykle aplikují pomocí kontextového rozhraní API.

V dalším testu otestujeme vykreslování podřízených uzlů v rámci nadřazeného uzlu.

Nejprve si vytvoříme novou renderovací komponentu NameRenderer :

//<root>/src/components/name/nameRenderer.js
import React from "react";

export const NameRenderer = ({ name }) => <div role="output" id="output">Hello, {name}</div>;

Spuštění naší testovací sady by mělo vést ke dvěma selháním:

✓ should render component
✕ should render default state
✕ should display name when user types into input field

Je to proto, že uplatňujeme prvek <div#output> který již v této komponentě neexistuje, ale je vykreslen v jiné komponentě. Toto je vedlejší efekt používání shallow - vykreslí pouze uzly v rámci aktuálního komponenta.

Pomocí extrémně užitečné .debug() funkce utility (k dispozici také s mount ), můžeme vidět, že <div> byl nahrazen NameRenderer komponent:

<Fragment>
  <form autoComplete="off">
    <input type="hidden" autoComplete="false" />
    <label htmlFor="name">
      Name:
    </label>
    <input aria-label="name-input" type="text" id="name" name="name" value="" onChange={[Function: handleChange]} />
  </form>
  <NameRenderer name="" />
</Fragment>

Enzyme nám poskytuje dive() nástroj na ShallowWrapper API, které nám umožňuje vykreslovat podřízené uzly jeden po druhém. Buďte však opatrní, protože to může být, a také spouštěcí efekty, události životního cyklu a další odložené a asynchronní akce obsažené v této komponentě. Pomocí potápění , také rozšiřujete hranici toho, co je test komponent.

Pojďme aktualizovat náš první nefunkční test, should render default state implementací dive() funkce.

//<root>/src/components/name/name.test.js
it("should render default state", () => {
  expect(component.exists(NameRenderer)).toBe(true);
  expect(component.find(NameRenderer).dive().find("#output").text()).toBe("Hello, ");
});

Spuštěním naší testovací sady získáme:

✓ should render component
✓ should render default state
✕ should display name when user types into input field

Úspěch! Pojďme aktualizovat poslední testovací případ, který kontroluje, zda je výstup vykreslen:

//<root>/src/components/name/name.test.js
it("should display name when user types into input field", () => {
  component.find("input#name").simulate("change", { target: { value: "Charles" } });
  expect(component.find(NameRenderer).dive().find("#output").text()).toBe("Hello, Charles");
});

Zkontrolujeme to znovu pomocí npm t ...

✓ should render component
✓ should render default state
✓ should display name when user types into input field

Velký úspěch! V tomto omezeném případě jsme představili novou komponentu a otestovali, že rekvizity jsou úspěšně předány komponentě a vykresleny na virtuálním domku.

Je třeba se vyhnout testování vnořených uzlů na mělčině

Kód:Fáze 5 – Aktualizace enzymových testů.

V tomto bodě máme zásadní problém s našimi testy, který porušuje koncept testování jednotek. Unit testy jsou určeny k testování jednotlivých jednotek zdrojového kódu s minimální integrací tam, kde je to možné. V našem příkladu testujeme naše Name komponentu, ale také integraci do NameRenderer a to NameRenderer vykresluje správně!

Pojďme to napravit vytvořením testovací sady pro NameRenderer a úpravou testů jednotek v Name .

Pojďme vytvořit náš nameRenderer.test.js soubor, s naším zařízením nastaveným:

// <root>/src/components/name/nameRenderer.test.js
import { NameRenderer } from "./nameRenderer";
import React from "react";
import { shallow } from "enzyme";

describe("Component: NameRenderer", () => {

  let component = null;

  beforeEach(() => {
    component = shallow(<NameRenderer />);
  })

  afterEach(() => {
    component = null;
  });

});

I když naše NameRenderer komponenty trvá { name: string } prop, tu jsme nedefinovali, protože můžeme použít .setProps() utility funkce pro aktualizaci hodnoty a simulaci změny vstupní hodnoty.

Nejprve odstraníme výraz výchozí hodnoty z name.test.js testovací sadu a vytvořte nový výraz s výchozí hodnotou v nameRenderer.test.js testovací sadu, dotazování prvku s id #output :

// <root>/src/components/name/nameRenderer.test.js
it("should not render name", () => {
  expect(component.find("#output").text()).toBe("Hello, ");
});

Pro simulaci vstupních rekvizit při výměně součásti můžeme použít setProps a předejte novou hodnotu pro name . Přidejte to do našeho testu a ověřte stav změn komponenty s novou hodnotou:

// <root>/src/components/name/nameRenderer.test.js
it("should render name from prop", () => {
  component.setProps({ name: "Charles" });
  expect(component.find("#output").text()).toBe("Hello, Charles");
});

Probíhá naše testy s npm t by nám měl dát 4 úspěšné testy:

PASS  src/components/name/nameRenderer.test.js
  Component: NameRenderer
    ✓ should not render name
    ✓ should render name from prop

 PASS  src/components/name/name.test.js
  Component: Name
    ✓ should render component
    ✓ should display name when user types into input field

Velký úspěch, ale stále máme přetrvávající problém s testováním NameRenderer komponenta v name.test.js testovací sada. Pojďme to nyní aktualizovat. Nemusíme ověřovat měnící se text v komponentě, potřebujeme pouze ověřovat, že se mění vlastnosti podřízené komponenty.

Můžeme to udělat pomocí .props() užitná funkce a ověřování hodnoty rekvizit. Když použijeme .props() , vrátí mapu klíč/hodnota objektu všech vlastností předávaných komponentě. V našem testu tedy vrací { name: 'Charles' };

Pojďme aktualizovat náš test a potvrdit pomocí rekvizit:

// <root>/src/components/name/name.test.js
it("should display name when user types into input field", () => {
  component.find("input#name").simulate("change", { target: { value: "Charles" } });
  expect(component.find(NameRenderer).props()).toStrictEqual({name: "Charles"});
});

Aktualizovali jsme naše testovací sady, abychom odstranili duplicitní testovací úsilí, a vytvořili jsme testy proti naší komponentě rendereru. Podívejme se na použití mount pro práci s háky React.

Testování háčků s mount

Kód:Fáze 6 - Testování háčků s mount .

Testování háčků v Reactu lze provést několika způsoby; pomocí act() provádět synchronní aktualizace dom pro vykreslené prvky s ReactDOM , použijte funkce testování háku z react-testing-library nebo použijte mount() s enzymem. Podívejme se, jak to děláme s enzymem.

I když existují omezení ohledně toho, čeho můžeme dosáhnout při testování háku pomocí shallow , tato omezení u mount neexistují . Uvědomte si však, že odložené a asynchronní akce fungují aktualizovat dom a můžete hlásit chyby, a i když nemusíte nutně řešit asynchronní chyby, měli byste zacházet s nimi, jako žert nedělá nahlásit neúspěšné asynchronní sliby jako chyby, pokud je nezpracujete správně.

Vytvořme naši komponentu, která bude mluvit s json zástupným rozhraním API, a aktualizujeme komponentu, když asynchronní požadavek z useEffect háček je úspěšný.

// <root>/src/components/todo/todo.js
import React, { useEffect, useState } from "react";

export const Todo = () => {
  const [todo, setTodo] = useState(undefined);
  useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/todos/1`)
      .then(response => response.json())
      .then(json => setTodo(json));
  }, []);

  return todo 
    ? <div id="todo">{todo.id}: {todo.title}{todo.completed ? " [completed]" : null}</div>
    : <div id="todo">Loading...</div>;
}

Protože používáme prohlížeč vestavěný v fetch knihovna, budeme se tomu muset vysmívat. Nebudeme inicializovat komponentu v našem beforeEach protože potřebujeme zabalit asynchronní volání do act aby bylo zajištěno, že události životního cyklu reakce a háky jsou správně volány.

// <root>/src/components/todo/todo.test.js
let data = {
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
};

beforeEach(() => {
  global.fetch = jest.fn(() => Promise.resolve({
    status: 200,
    data,
    json: () => data
  }));
});

Kdykoli testujeme připojenou komponentu, zejména tu s odloženými a asynchronními akcemi, které mohou aktualizovat uživatelské rozhraní, musíme test zabalit do act :

// <root>/src/components/todo/todo.test.js
import { act } from 'react-dom/test-utils';
...
await act(async () => {
  component = await mount(<Todo />);
});

Odložené akce v rámci act budou provedeny a uživatelské rozhraní bude aktualizováno, ale mohou nastat situace, kdy budou provedeny další odložené a asynchronní akce a komponenta může vyžadovat „obnovení“. Komponentu můžeme aktualizovat pomocí .update() užitková funkce.

// <root>/src/components/todo/todo.test.js
await act(async () => {
  component = await mount(<Todo />);
  component.update();
});

Pojďme to nyní dát dohromady a přidat testy, abychom zajistili existenci prvku, vykreslení prvku a fetch se volá pomocí rozhraní todo api - nemusíte musí být připojen k internetu, aby testy prošly. Jak můžete vidět níže, naše tvrzení jsou stejná mezi shallow a mount , přičemž primární rozdíl je v tom, že odložené a asynchronní akce jsou volány automaticky spolu s vykreslováním dalších komponent.

it("should render element 'todo'", async () => {
  await act(async () => {
    component = await mount(<Todo />);
  });
  expect(component.exists("#todo")).toBe(true);
});

it("should show todo once async resolves", async () => {
  await act(async () => {
    component = await mount(<Todo />);
    component.update();
  });
  expect(component.find("#todo").text()).toBe("1: delectus aut autem");
})

it("should call fetch", async () => {
  await act(async () => {
    component = await mount(<Todo />);
    component.update();
  });

  expect(global.fetch).toHaveBeenLastCalledWith("https://jsonplaceholder.typicode.com/todos/1");
})

Když spustíme náš npm t měli bychom vidět zelenou přes palubu:

 PASS  src/components/name/nameRenderer.test.js
  Component: NameRenderer
    ✓ should not render name
    ✓ should render name from prop

 PASS  src/components/name/name.test.js
  Component: Name
    ✓ should render component
    ✓ should display name when user types into input field

 PASS  src/components/todo/todo.test.js
  Component: Todo
    ✓ should render element 'todo'
    ✓ should show todo once async resolves
    ✓ should call fetch

Velký úspěch!

Souhrn

Podívali jsme se na případy použití pro shadow a mount , pokryl rozdíly mezi dvěma testovacími metodami a prokázal schopnost mount spustit useEffect háčky, které aktualizují uživatelské rozhraní.

Enzym dokáže mnohem více, než to, co jsme pokryli. Jeho bohatá syntaxe dotazování umožňuje hluboké porovnávání a testování komponent, což zdaleka převyšuje to, co mohou nabídnout další nejlepší nástroje.

Při použití spolu s jest , enzym je skvělý nástroj, díky kterému je testování komponent hračkou a odstraňuje skutečnou bolest z rovnice. Při testování funkčnosti specifické pro React (udělal useEffect nechat si zavolat? 🤔), nezapomeňte otestovat výsledek, nikoli samotnou akci React.