- React.js: Wprowadzenie do kursu od podstaw
- Poznaj React.js
- Pierwszy komponent w React.js
- Props czyli atrybuty w React.js
- Podział na komponenty w React.js
- Klasy jako komponenty React.js
- Interakcja z komponentami React.js
- Stan komponentów React.js
- State w React.js 2
- Metody cyklu życia komponentu w React.js
- React.js w przykładach: filtrowanie statycznej listy
- Tworzenie aplikacji React.js dzięki create-react-app
- React.js na GitHub Pages dzięki create-react-app
- Testowanie aplikacji React.js — podstawy Enzyme
- Testowanie React.js w Enzyme — props, state i interakcje
- Poprawne bindowanie funkcji w React.js
- Odpowiadam na pytania: Babel, ECMAScript, destrukturyzacja, onClick, className
- Komunikacja pomiędzy komponentami w React.js
- Komunikacja z API w React.js
- Formularze w React.js — kontrolowane komponenty
- Formularze w React.js — niekontrolowane komponenty
- Odpowiadam na pytania: props, nawiasy klamrowe, funkcje vs klasy, import react
- TDD w React.js z pomocą react-testing-library
- Flux i Redux: globalny store i jednokierunkowy przepływ danych
- React + Redux — kurs: wprowadzenie i podstawy
- React + Redux — filtrowanie listy, proste selektory
- Projektowanie komponentów: Presentational & Container Components
- Asynchroniczność w Redux: redux-thunk
- Kiedy używać state, a kiedy Redux?
- Nowe metody cyklu życia: getDerivedStateFromProps i getSnapshotBeforeUpdate
- Leniwe ładowanie komponentów w React dzięki import
- Higher Order Reducers — Redux i powtarzanie kodu
- React Hooks — wprowadzenie i motywacja
- React Hooks: useState, czyli stan w komponentach funkcyjnych
- React Hooks: useState — wiele stanów, callbacki i inne niuanse
- React Hooks: useEffect — efekty uboczne w komponencie
- React Hooks a żądania do API
- useReducer — przenoszenie logiki poza komponent
- useMemo, useCallback, czyli rozwiązanie problemów ze zmieniającymi się propsami
- Wady React Hooks
- React Hooks: Piszemy własne hooki!
Jak tworzyć komponenty w React.js zgodnie z TDD dzięki react-testing-library? Jak zamockować axios
? Jak napisać testy odporne na refactoring? To i kilka innych sztuczek w artykule poniżej 🙂
Trzymanie się zasad TDD (Test-Driven Development) pisząc aplikacje po stronie front-endu w React.js może wydawać się trudniejsze niż testowanie kodu po stronie back-endu.
Musimy w jakiś sposób wyrenderować nasz komponent, zasymulować interakcje użytkownika z przeglądarką, reagować na zmiany propsów i stanu naszego komponentu, a na koniec jeszcze przetestować asynchroniczne metody wywołane przez na przykład kliknięcie w przycisk na stronie.
Aby pokryć te wszystkie scenariusze w naszych testach, dochodzi często do sytuacji, w których stają się one nieczytelne, jeden zależy od drugiego, mockujemy na potęgę i w rezultacie mamy testy napisane wg. antypatternów.
Szanuj swój czas
Z moich obserwacji dużo osób tworzy cały działający komponent i dopiero wtedy zabiera się za pisanie do niego testów, a następnie okazuje się, że nie da się przetestować go w obecnej implementacji i trzeba go przepisać. Tracimy na tym czas, cierpliwość i pieniądze pracodawcy.
Dostępne rozwiązania
Na nasze szczęście istnieje wiele bibliotek, które rozwiązują nam problem renderowania komponentu (np. Enzyme), mockowania odpowiedzi z servera (np. MockAxios), ale często mają nie do końca jasne API jak w przypadku tego pierwszego — czym do cholery różni się od siebie Shallow, Mount i Render i którego powinienem użyć?!?
O projekcie
Na potrzeby artykułu stworzymy małą aplikację, która po kliknięciu w przycisk będzie pobierała z zewnętrznego API losowy kawał, w którym główną rolę pełni Chuck Norris. Będziemy stopniowo pisać testy z pomocą react-testing-library, a następnie tworzyć komponent i starać się żeby testy przeszły.
Pisząc testy będziemy mieć w głowie to zdanie:
The more your tests resemble the way your software is used, the more confidence they can give you.
— Kent C. Dodds (@kentcdodds) March 23, 2018
Zaczynamy
Projekt stworzymy z boilerplate create-react-app, Axios użyjemy do pobierania danych z zewnętrznego API, do uruchamiania testów Jest’a, do mockowania zewnętrznego API MockAxios, a do renderowania komponentów, triggerowania akcji i obsługi asynchronicznych metod react-testing-library — świetnej i ultra lekkiej biblioteki stworzonej przez cytowanego już wcześniej Kent C. Dodds.
Generujemy projekt z create-react-app wg. instrukcji, a następnie instalujemy dodatkowe zależności (do stworzenia projektu możemy użyć także CodeSandbox):
npm install axios
npm install --save-dev axios-mock-adapter react-testing-library
Struktura
Tworzymy podobną sktrukture plików jak poniżej:
- src
- __tests__
- jokeGenerator.test.js
- joke.js
- jokeGenerator.js
- index.js
Piszemy pierwszy test
Zaczniemy od napisania testu do komponentu Joke, którego funkcją będzie wyświetlenie tekstu przekazanego przez propsy (jokeGenerator.test.js
):
test("Joke komponent otrzymuje propsy, a następnie renderuje text", () => {
const { getByTestId } = render(
<Joke text="The funniest joke this year." />
);
expect(getByTestId("joke-text")).toHaveTextContent(
"The funniest joke this year."
);
});
Już tłumaczę co tu się dzieje. Idąc od góry widzimy funkcję render
zaimportowaną z paczki react-testing-library. Przekazujemy do niej nasz jeszcze nie istniejący komponent.
Funkcja ta zwraca obiekt zawierający kilka przydatnych metod (pełna lista metod) min. getByTestId
— zwraca nam element HTML przyjmując data-testid
jako argument.
Czym jest data-testid
? Jest to unikalny atrybut elementu na podstawie którego możemy napisać odpowiedni selektor HTML.
Korzystając z tej metody możemy napisać expecta, który oczekuje, że innerHTML będzie równy „The funniest joke this year„.
Dzięki data-testid
nasze testy stają się odporne na refactoring ponieważ polegamy na wartościach, które w kodzie już raczej się nie zmienią. Należy jednak korzystać z tego z rozwagą, chcemy przecież aby nasz test odzwierciedlał to, jak użytkownik będzie z aplikacji korzystał.
Dlatego najlepiej stosować data-testid
, gdy metody getByText
/queryByText
zawiodą.
npm test
Uruchamiamy testy i widzimy:
Joke is not defined
Tego się spodziewaliśmy! Joke
jeszcze nie istnieje, stworzyliśmy do tej pory tylko pusty plik joke.js
.
Napisaliśmy test, w którym jasno widać czego od komponentu oczekujemy. Teraz naszym zadaniem jest nie dotykając już testu sprawić, aby test przeszedł (joke.js
):
export default ({ text }) => (
<div data-testid="joke-text">
{text}
</div>
);
Po przeładowaniu testów jeśli zrobiłaś wszystko tak jak ja, test powinien przejść 🙂
Drugi komponent
Zadaniem drugiego komponentu będzie pobranie losowego kawału z API po kliknięciu w przycisk, zapisanie go w state komponenetu i wyrenderowanie dzięki znanemu już nam Joke
.
Startujemy oczywiście od napisania testu. Jest to większy komponent, zatem test będziemy pisać stopniowo i będziemy starali się żeby jak najczęściej był „zielony”.
test("Komponent 'JokeGenerator' pobiera randomowego suchara i go renderuje", async () => {
const { getByText } = render(<JokeGenerator />);
expect(getByText("Brak suchara")).toBeTruthy();
});
Widzimy znaną już nam funkcję render
, tylko tym razem wyciągamy z niej getByText
. Jak łatwo się domyśleć, zwraca nam element HTML z podanym przez nas tekstem jeśli takowy oczywiście istnieje.
Chuck Norris can overflow your stack just by looking at it.
Odświeżamy testy i mamy:
JokeGenerator is not defined
Wiemy co z tym zrobić:
export default class JokeGenerator extends React.Component {
render() {
return <div />;
}
}
Rezultat:
Unable to find an element with the text: **Brak suchara**. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Chcemy wyświetlić powyższy tekst gdy nie mamy w state żadnego kawału:
Wiemy co z tym zrobić:
export default class JokeGenerator extends React.Component {
state = {
joke: null
};
render() {
const { joke } = this.state;
return (
<React.Fragment>
{!joke && <div>Brak suchara</div>}
</React.Fragment>
);
}
}
Teraz chcę zasymulować kliknięcie w przycisk przez użytkownika i zobaczyć wiadomość, że mój kawał się ładuje, a domyślny tekst Brak Suchara znika. Użyjemy w tym celu metody Simulate
.
import { render, Simulate } from "react-testing-library"
Simulate.click(getByTestId("laduj-suchara"));
expect(queryByText("Brak suchara")).toBeNull();
expect(querybyText("Ładuję...")).not.toBeNull();
queryByText
różni się od getByText
tym, że ten pierwszy gdy nie znajdzie elementu zwraca null
, a ten drugi rzuca błędem.
Po przeładowaniu testów:
Unable to find an element by: [data-testid="laduj-suchara"]
Tworzymy buttona i przy okazji metode która ustawi nam loading
state na true
.
export default class JokeGenerator extends React.Component {
state = {
joke: null,
loading: false
};
loadJoke = () => {
this.setState({loading: true });
};
render() {
const { joke, loading } = this.state;
return (
<React.Fragment>
{!joke && !loading && <div>Brak suchara</div>}
{loading && <div>Ładuję...</div>}
<button
onClick={this.loadJoke}
type="button"
data-testid="laduj-suchara"
>
Załaduj losowy kawał
</button>
</React.Fragment>
);
}
}
Testy elegancko przechodzą. Zamockujmy teraz odpowiedź z serwera używając MockAxios
.
import MockAxios from "axios-mock-adapter";
Zaraz nad pierwszym testem dopiszmy ten fragment kodu:
const mock = new MockAxios(axios, { delayResponse: Math.random() * 500 });
afterAll(() => mock.restore());
Na początku drugiego testu, w którym testujemy JokeGenerator
dodajmy:
mock.onGet().replyOnce(200, {
value: {
joke: "Really funny joke!"
}
});
A na końcu tego samego testu:
await wait(() => expect(queryByText("Ładuję...")).toBeNull());
expet(queryByTestId("joke-text")).toBeTruthy();
Metoda wait
(importujemy ją tak samo jak Simulate
i render
) czeka (domyślnie 4500ms) na callbacka dopóki ten nie przestanie zwracać erroru. Interwał w jakim sprawdzane jest wyrażenie w callbacku to domyślnie 50ms.
Dzięki tej metodzie możemy testować min. asynchroniczne działania w naszej aplikacji.
Co ciekawe wait
dostępne jest jako oddzielna paczka (react-testing-library
z tej paczki korzysta). Stworzył ją Łukasz Gandecki z The Brain Software House.
Po tych modyfikacjach powinniśmy dostać taki błąd:
Expected value to be truthy, instead received:
null
Aby nasz test zaczął ponownie przechodzić musimy zmodyfikować naszą metode loadJoke
:
loadJoke = async () => {
this.setState({ loading: true });
const { data: { value: { joke } } } = await axios.get("https://api.icndb.com/jokes/random");
this.setState({ loading: false, joke });
};
oraz wyrenderować nasz kawał przy użyciu Joke
:
{joke && !loading && <Joke text={joke} />}
Test powinien ponownie zrobić się zielony, a my mamy pewność, że wszystko działa.
Zauważcie, że jeszcze ani razu nie otworzyliśmy przeglądarki i nie przetestowaliśmy tego ręcznie, ale dzięki temu w jak pisaliśmy testy (w sposób w jaki użytkownik normalnie korzysta z aplikacji) mamy 100% pewność, że nasza mała aplikacja po prostu działa.
Dodajmy na koniec JokeGenerator
do index.js
i odpalmy przeglądarke:
const App = () => (
<div style={styles}>
<JokeGenerator />
</div>
);
Bonus
Sposób w jaki napisaliśmy nasze testy umożliwia nam wykorzystanie ich jako testów e2e bez dodawania ani jednej linijki kodu.
Wystarczy, że zakomentujemy fragmenty kodu odpowiedzialne za mockowanie Axios’a i gotowe! Uruchom teraz testy, a będą korzystać z prawdziwego API.
Podsumowanie
W razie problemów kod całego projektu dostępny jest na CodeSandbox.
Zachęcam do zapoznania się z pełną dokumentacją react-testing-library
. Mamy do dyspozycji więcej metod do znajdywania elementów w naszym wirtualnym DOM-ie, zwracania wartości tekstu z elementu itd. Naucz się React Testing Library na szkoleniu!
Mam nadzieję, że dzięki mnie czegoś się dzisiaj nauczyliście i wykorzystacie parę technik w Waszych projektach.
- React.js: Wprowadzenie do kursu od podstaw
- Poznaj React.js
- Pierwszy komponent w React.js
- Props czyli atrybuty w React.js
- Podział na komponenty w React.js
- Klasy jako komponenty React.js
- Interakcja z komponentami React.js
- Stan komponentów React.js
- State w React.js 2
- Metody cyklu życia komponentu w React.js
- React.js w przykładach: filtrowanie statycznej listy
- Tworzenie aplikacji React.js dzięki create-react-app
- React.js na GitHub Pages dzięki create-react-app
- Testowanie aplikacji React.js — podstawy Enzyme
- Testowanie React.js w Enzyme — props, state i interakcje
- Poprawne bindowanie funkcji w React.js
- Odpowiadam na pytania: Babel, ECMAScript, destrukturyzacja, onClick, className
- Komunikacja pomiędzy komponentami w React.js
- Komunikacja z API w React.js
- Formularze w React.js — kontrolowane komponenty
- Formularze w React.js — niekontrolowane komponenty
- Odpowiadam na pytania: props, nawiasy klamrowe, funkcje vs klasy, import react
- TDD w React.js z pomocą react-testing-library
- Flux i Redux: globalny store i jednokierunkowy przepływ danych
- React + Redux — kurs: wprowadzenie i podstawy
- React + Redux — filtrowanie listy, proste selektory
- Projektowanie komponentów: Presentational & Container Components
- Asynchroniczność w Redux: redux-thunk
- Kiedy używać state, a kiedy Redux?
- Nowe metody cyklu życia: getDerivedStateFromProps i getSnapshotBeforeUpdate
- Leniwe ładowanie komponentów w React dzięki import
- Higher Order Reducers — Redux i powtarzanie kodu
- React Hooks — wprowadzenie i motywacja
- React Hooks: useState, czyli stan w komponentach funkcyjnych
- React Hooks: useState — wiele stanów, callbacki i inne niuanse
- React Hooks: useEffect — efekty uboczne w komponencie
- React Hooks a żądania do API
- useReducer — przenoszenie logiki poza komponent
- useMemo, useCallback, czyli rozwiązanie problemów ze zmieniającymi się propsami
- Wady React Hooks
- React Hooks: Piszemy własne hooki!
błąd przy pierwszym tescie import ReactDOM from 'react-dom';
szukałem na necie ale np rozwiązanie z przeinstalowaniem react-dom nie pomogło
Jaki błąd? Skąd ten kod? Jesteś pewien, że nie miało być
import * as ReactDOM from 'react-dom'
?Mam pytanie nie na temat.
Czy mógłbyś zrobić zestawienie forów, na których można zadawać pytania po polsku o frontend? Prócz tych grup facebookowych, chodzi o zwykłe fora, ale też tematyczne, gdzie można liczyć na jakąś odpowiedź i w miarę szybką. 🙂
Możesz zadawać pytania tutaj 😉
Będę musiał temu dać szansę w jakimś mniejszym projekcie. Dzięki za wprowadzenie.
[…] Michał Baranowski — TDD w React.js z pomocą react-testing-library […]