Programowanie React.js sterowane testami: Testowanie jednostkowe React.js z użyciem Enzyme i Jest

Opublikowany: 2022-03-11

Według Michaela Feathersa każdy fragment kodu, który nie jest testowany, jest kodem przestarzałym. Dlatego jednym z najlepszych sposobów na uniknięcie tworzenia starszego kodu jest korzystanie z programowania opartego na testach (TDD).

Chociaż dostępnych jest wiele narzędzi do testowania jednostkowego JavaScript i React.js, w tym poście użyjemy Jest i Enzyme do stworzenia komponentu React.js z podstawową funkcjonalnością przy użyciu TDD.

Dlaczego warto używać TDD do tworzenia komponentu React.js?

TDD przynosi wiele korzyści Twojemu kodowi — jedną z zalet wysokiego pokrycia testami jest to, że umożliwia łatwą refaktoryzację kodu przy jednoczesnym zachowaniu czystości i funkcjonalności kodu.

Jeśli tworzyłeś wcześniej komponent React.js, zdałeś sobie sprawę, że kod może rosnąć bardzo szybko. Wypełnia się wieloma złożonymi warunkami spowodowanymi przez oświadczenia związane ze zmianami stanu i zgłoszeniami serwisowymi.

Każdy komponent, w którym brakuje testów jednostkowych, ma przestarzały kod, który staje się trudny do utrzymania. Moglibyśmy dodać testy jednostkowe po utworzeniu kodu produkcyjnego. Możemy jednak narazić się na ryzyko przeoczenia niektórych scenariuszy, które powinny zostać przetestowane. Tworząc najpierw testy, mamy większą szansę na pokrycie każdego scenariusza logicznego w naszym komponencie, co ułatwiłoby refaktoryzację i utrzymanie.

Jak testujemy jednostkowo komponent React.js?

Istnieje wiele strategii, których możemy użyć do przetestowania komponentu React.js:

  • Możemy zweryfikować, czy dana funkcja w props została wywołana po wywołaniu określonego zdarzenia.
  • Możemy również uzyskać wynik funkcji render , biorąc pod uwagę aktualny stan komponentu i dopasować go do predefiniowanego układu.
  • Możemy nawet sprawdzić, czy liczba dzieci składnika odpowiada oczekiwanej ilości.

Aby wykorzystać te strategie, użyjemy dwóch narzędzi, które przydadzą się do pracy z testami w React.js: Jest i Enzyme.

Używanie Jest do tworzenia testów jednostkowych

Jest to framework testowy o otwartym kodzie źródłowym stworzony przez Facebooka, który świetnie integruje się z React.js. Zawiera narzędzie wiersza poleceń do wykonywania testów podobne do tego, co oferują Jasmine i Mocha. Pozwala nam również tworzyć atrapy funkcji z prawie zerową konfiguracją i zapewnia naprawdę ładny zestaw dopasowań, który ułatwia odczytywanie asercji.

Co więcej, oferuje naprawdę fajną funkcję o nazwie „testowanie migawek”, która pomaga nam sprawdzić i zweryfikować wynik renderowania komponentów. Wykorzystamy testy migawkowe, aby przechwycić drzewo komponentu i zapisać je w pliku, którego możemy użyć do porównania go z drzewem renderowania (lub cokolwiek, co przekazujemy do funkcji expect jako pierwszego argumentu).

Używanie enzymu do montowania komponentów React.js

Enzym zapewnia mechanizm montowania i przechodzenia przez drzewa komponentów React.js. Pomoże nam to uzyskać dostęp do jego własnych właściwości i stanu, a także jego właściwości podrzędnych w celu uruchomienia naszych asercji.

Enzym oferuje dwie podstawowe funkcje montowania komponentów: shallow i mount . shallow funkcja ładuje do pamięci tylko główny komponent, podczas gdy mount ładuje pełne drzewo DOM.

Połączymy Enzyme i Jest, aby zamontować komponent React.js i uruchomić na nim asercje.

Kroki TDD w celu stworzenia komponentu reakcji

Konfigurowanie naszego środowiska

Możesz rzucić okiem na to repozytorium, które ma podstawową konfigurację do uruchomienia tego przykładu.

Używamy następujących wersji:

 { "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }

Tworzenie komponentu React.js za pomocą TDD

Pierwszym krokiem jest stworzenie nieudanego testu, który spróbuje wyrenderować komponent React.js przy użyciu płytkiej funkcji enzymu.

 // MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });

Po uruchomieniu testu otrzymujemy następujący błąd:

 ReferenceError: MyComponent is not defined.

Następnie tworzymy komponent zawierający podstawową składnię, dzięki której test przejdzie pomyślnie.

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }

W następnym kroku upewnimy się, że nasz komponent renderuje predefiniowany układ interfejsu użytkownika za pomocą funkcji toMatchSnapshot z Jest.

Po wywołaniu tej metody Jest automatycznie tworzy plik migawki o nazwie [testFileName].snap , który jest dodawany do folderu __snapshots__ .

Ten plik reprezentuje układ interfejsu użytkownika, którego oczekujemy od renderowania komponentów.

Jednak biorąc pod uwagę, że staramy się wykonać czyste TDD, powinniśmy najpierw utworzyć ten plik, a następnie wywołać funkcję toMatchSnapshot , aby test się nie powiódł.

Może to zabrzmieć trochę mylące, biorąc pod uwagę, że nie wiemy, jakiego formatu używa Jest do reprezentowania tego układu.

Możesz ulec pokusie, aby najpierw wykonać funkcję toMatchSnapshot i zobaczyć wynik w pliku migawki, a to jest prawidłowa opcja. Jeśli jednak naprawdę chcemy korzystać z czystego TDD, musimy nauczyć się struktury plików migawek.

Plik migawki zawiera układ zgodny z nazwą testu. Oznacza to, że jeśli nasz test ma taką formę:

 desc("ComponentA" () => { it("should do something", () => { … } });

Powinniśmy to określić w sekcji eksportu: Component A should do something 1 .

Możesz przeczytać więcej o testowaniu migawek tutaj.

Dlatego najpierw tworzymy plik MyComponent.test.js.snap .

 //__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;

Następnie tworzymy test jednostkowy, który sprawdzi, czy migawka pasuje do elementów podrzędnych komponentu.

 // MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...

Możemy uznać components.getElements za wynik metody render.

Przekazujemy te elementy do metody expect w celu przeprowadzenia weryfikacji względem pliku migawki.

Po wykonaniu testu otrzymujemy następujący błąd:

 Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []

Tylko mówi nam, że wynik z component.getElements nie pasuje do migawki. Zatem sprawiamy, że ten test przechodzi, dodając element input w MyComponent .

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }

Następnym krokiem jest dodanie funkcjonalności do input poprzez wykonanie funkcji, gdy zmieni się jej wartość. Robimy to, określając funkcję we właściwości onChange .

Najpierw musimy zmienić migawkę, aby test się nie powiódł.

 //__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;

Wadą modyfikowania najpierw migawki jest to, że ważna jest kolejność właściwości (lub atrybutów).

Jest posortuje alfabetycznie właściwości otrzymane w funkcji expect przed zweryfikowaniem jej względem migawki. Powinniśmy więc je określić w tej kolejności.

Po wykonaniu testu otrzymujemy następujący błąd:

 Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]

Aby ten test przebiegł pomyślnie, możemy po prostu udostępnić pustą funkcję onChange .

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }

Następnie upewniamy się, że stan komponentu zmieni się po wywołaniu zdarzenia onChange .

Aby to zrobić, tworzymy nowy test jednostkowy, który wywoła funkcję onChange w danych wejściowych, przekazując zdarzenie w celu naśladowania rzeczywistego zdarzenia w interfejsie użytkownika.

Następnie sprawdzamy, czy stan komponentu zawiera klucz o nazwie input .

 // MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });

Otrzymujemy teraz następujący błąd.

 Expected value to be defined, instead received undefined

Oznacza to, że składnik nie ma właściwości w stanie o nazwie input .

Test przechodzi przez ustawienie tego wpisu w stanie komponentu.

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }

Następnie musimy upewnić się, że w nowym wpisie stanu jest ustawiona wartość. Tę wartość wydobędziemy z wydarzenia.

Stwórzmy więc test, który upewni się, że stan zawiera tę wartość.

 // MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""

W końcu przeprowadzamy ten test, pobierając wartość ze zdarzenia i ustawiając ją jako wartość wejściową.

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }

Po upewnieniu się, że wszystkie testy przeszły pomyślnie, możemy dokonać refaktoryzacji naszego kodu.

Możemy wyodrębnić funkcję przekazaną we właściwości onChange do nowej funkcji o nazwie updateState .

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }

Mamy teraz prosty komponent React.js stworzony przy użyciu TDD.

Streszczenie

W tym przykładzie próbowaliśmy użyć czystego TDD, pisząc każdy krok, pisząc jak najmniej kodu, który może się nie powieść i przejść testy.

Niektóre kroki mogą wydawać się niepotrzebne i możemy mieć pokusę ich pominięcia. Jednak za każdym razem, gdy pominiemy jakikolwiek krok, w końcu użyjemy mniej czystej wersji TDD.

Korzystanie z mniej rygorystycznego procesu TDD jest również prawidłowe i może działać dobrze.

Moją radą dla Ciebie jest unikanie pomijania jakichkolwiek kroków i nie czuj się źle, jeśli okaże się to trudne. TDD to technika, która nie jest łatwa do opanowania, ale zdecydowanie warto to robić.

Jeśli chcesz dowiedzieć się więcej o TDD i powiązanym z nim rozwoju opartym na zachowaniu (BDD), przeczytaj artykuł Twój szef nie doceni TDD autorstwa innego toptalera, Ryana Wilcoxa.