Jak komponenty React ułatwiają testowanie interfejsu użytkownika
Opublikowany: 2022-03-11Testowanie zaplecza jest łatwe. Wybierasz wybrany język, łączysz go z ulubionym frameworkiem, piszesz testy i klikasz „uruchom”. Twoja konsola mówi „Yay! To działa!" Twoja usługa ciągłej integracji przeprowadza testy przy każdym naciśnięciu, życie jest wspaniałe.
Oczywiście, programowanie sterowane testami (TDD) jest początkowo dziwne, ale przewidywalne środowisko, wiele programów uruchamiających testy, narzędzia testowe wbudowane we frameworki i wsparcie ciągłej integracji ułatwiają życie. Pięć lat temu myślałem, że testy są rozwiązaniem każdego problemu, jaki kiedykolwiek miałem.
Potem Backbone stał się duży.
Wszyscy przeszliśmy na front-end MVC. Nasze testowalne backendy stały się gloryfikowanymi serwerami baz danych. Nasz najbardziej skomplikowany kod został przeniesiony do przeglądarki. A nasze aplikacje nie były już testowalne w praktyce.
To dlatego, że testowanie kodu front-endu i komponentów interfejsu użytkownika jest dość trudne.
Nie jest tak źle, jeśli chcemy tylko sprawdzić, czy nasze modele dobrze się zachowują. Lub, że wywołanie funkcji zmieni właściwą wartość. Wszystko, co musimy zrobić do testów jednostkowych React, to:
- Napisz dobrze uformowane, izolowane moduły.
- Użyj testów Jasmine lub Mocha (lub czegokolwiek) do uruchamiania funkcji.
- Użyj biegacza testowego, takiego jak Karma lub Chutzpah.
Otóż to. Nasz kod jest testowany jednostkowo.
Kiedyś przeprowadzanie testów front-endu było najtrudniejszą częścią. Każdy framework miał swoje własne pomysły i w większości przypadków kończyło się to oknem przeglądarki, które ręcznie odświeżałeś za każdym razem, gdy chciałeś przeprowadzić testy. Oczywiście zawsze byś zapomniała. Przynajmniej wiem, że tak.
W 2012 roku Vojta Jina wypuścił biegacza Karma (wówczas Testacular). Dzięki Karmie testowanie front-end staje się pełnoprawnym obywatelem łańcucha narzędzi. Nasze testy React działają w terminalu lub na serwerze ciągłej integracji, uruchamiają się ponownie, gdy zmieniamy plik, a nawet możemy testować nasz kod w wielu przeglądarkach jednocześnie.
Czego chcieć więcej? Cóż, żeby przetestować nasz kod front-endowy.
Testowanie front-endu wymaga czegoś więcej niż tylko testów jednostkowych
Testy jednostkowe są świetne: to najlepszy sposób, aby sprawdzić, czy algorytm za każdym razem postępuje właściwie, lub sprawdzić naszą logikę walidacji danych wejściowych, przekształcenia danych lub inne izolowane operacje. Testowanie jednostkowe jest idealne dla podstaw.
Ale w kodzie frontonu nie chodzi o manipulowanie danymi. Chodzi o zdarzenia użytkowników i renderowanie odpowiednich widoków we właściwym czasie. Front-endy dotyczą użytkowników.
Oto, co chcemy zrobić:
- Testuj zdarzenia użytkownika React
- Przetestuj reakcję na te wydarzenia
- Upewnij się, że właściwe rzeczy wyświetlają się we właściwym czasie
- Uruchom testy w wielu przeglądarkach
- Ponownie uruchom testy zmian w plikach
- Pracuj z systemami ciągłej integracji, takimi jak Travis
Przez dziesięć lat, kiedy to robiłem, nie znalazłem przyzwoitego sposobu na przetestowanie interakcji z użytkownikiem i renderowanie widoku, dopóki nie zacząłem grzebać w React.
Testowanie jednostkowe React: komponenty interfejsu użytkownika
React to najłatwiejszy sposób na osiągnięcie tych celów. Po części dlatego, że zmusza nas to do projektowania aplikacji przy użyciu testowalnych wzorców, a po części z powodu fantastycznych narzędzi testowych React.
Jeśli nigdy wcześniej nie korzystałeś z Reacta, powinieneś zajrzeć do mojej książki React+d3.js . Jest nastawiony na wizualizacje, ale powiedziano mi, że to „niesamowite, lekkie wprowadzenie” do Reacta.
React zmusza nas do budowania wszystkiego jako „komponentów”. Możesz myśleć o komponentach React jako widżetach lub fragmentach HTML z pewną logiką. Przestrzegają wielu najlepszych zasad programowania funkcjonalnego, z wyjątkiem tego, że są obiektami.
Na przykład, mając ten sam zestaw parametrów, komponent React zawsze wygeneruje ten sam wynik. Bez względu na to, ile razy jest renderowany, bez względu na to, kto to renderuje, bez względu na to, gdzie umieścimy wyjście. Zawsze to samo. Dzięki temu nie musimy wykonywać skomplikowanych rusztowań, aby testować komponenty React. Dbają tylko o swoje właściwości, nie jest wymagane śledzenie zmiennych globalnych i obiektów konfiguracyjnych.
Osiągamy to w dużej mierze poprzez unikanie stanu. Nazwalibyście to referencyjną przejrzystością w programowaniu funkcjonalnym. Nie sądzę, że w React istnieje na to specjalna nazwa, ale oficjalna dokumentacja zaleca unikanie używania państwa w jak największym stopniu.
Jeśli chodzi o testowanie interakcji z użytkownikami, React zapewnia nam obsługę zdarzeń związanych z wywołaniami zwrotnymi funkcji. Skonfigurowanie szpiegów testowych i upewnienie się, że zdarzenie kliknięcia wywołuje odpowiednią funkcję, jest łatwe. A ponieważ komponenty React renderują się same, możemy po prostu wywołać zdarzenie kliknięcia i sprawdzić kod HTML pod kątem zmian. Działa to, ponieważ komponent React dba tylko o siebie. Kliknięcie tutaj nic nie zmieni. Nigdy nie będziemy mieli do czynienia z zagnieżdżeniem obsługi zdarzeń, tylko z dobrze zdefiniowanymi wywołaniami funkcji.
Aha, a ponieważ React to magia, nie musimy się martwić o DOM. React używa tak zwanego wirtualnego DOM do renderowania komponentów do zmiennej JavaScript. A odniesienie do wirtualnego DOM to tak naprawdę wszystko, czego potrzebujemy do testowania komponentów React.
To całkiem słodkie.
TestUtils
React zawiera zestaw wbudowanych TestUtils
. Jest nawet zalecany biegacz testowy o nazwie Jest, ale mi się to nie podoba. Za chwilę wyjaśnię dlaczego. Po pierwsze, TestUtils
.
Uzyskujemy je, wykonując coś takiego jak require('react/addons').addons.TestUtils
. To jest nasz punkt wyjścia do testowania interakcji użytkowników i sprawdzania wyników.
React TestUtils
pozwala nam renderować komponent React poprzez umieszczenie jego DOM w zmiennej, zamiast wstawiać go na stronie. Na przykład, aby wyrenderować komponent React, zrobimy coś takiego:
var component = TestUtils.renderIntoDocument( <MyComponent /> );
Następnie możemy użyć TestUtils
, aby sprawdzić, czy wszystkie dzieci zostały wyrenderowane. Coś takiego:
var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );
findRenderedDOMComponentWithTag
zrobi to, na co wygląda: przejrzyj elementy potomne, znajdź szukany składnik i zwróć go. Zwrócona wartość będzie zachowywać się jak komponent React.
Następnie możemy użyć getDOMNode()
, aby uzyskać dostęp do surowego elementu DOM i przetestować jego wartości. Aby sprawdzić, czy tag h1
w komponencie mówi „Tytuł” , napiszemy to:
expect(h1.getDOMNode().textContent) .toEqual("A title");
Podsumowując, pełny test wyglądałby tak:
it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });
Fajną częścią jest to, że TestUtils pozwala nam również wyzwalać zdarzenia użytkownika. W przypadku zdarzenia kliknięcia napisalibyśmy coś takiego:
var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);
Symuluje to kliknięcie i wyzwala wszystkich potencjalnych słuchaczy, które powinny być metodami składowymi, które zmieniają dane wyjściowe, stan lub oba. Te detektory mogą w razie potrzeby wywołać funkcję na komponencie nadrzędnym.
Wszystkie przypadki są proste do przetestowania: zmieniony stan jest w component.state
, możemy uzyskać dostęp do wyjścia za pomocą normalnych funkcji DOM i wywołań funkcji za pomocą szpiegów.
Dlaczego nie jest?
Oficjalna dokumentacja Reacta zaleca używanie https://facebook.github.io/jest/ jako narzędzia do uruchamiania testów i frameworka testowego React. Jest zbudowany na Jasmine i używa tej samej składni. Oprócz wszystkiego, co dostajesz od Jasmine, Jest również kpi ze wszystkiego, z wyjątkiem testowanego komponentu. Teoretycznie jest to fantastyczne, ale mnie to denerwuje. Wszystko, czego jeszcze nie zaimplementowaliśmy lub co pochodzi z innej części bazy kodu, jest po prostu undefined
. Chociaż w wielu przypadkach jest to w porządku, może prowadzić do cichych błędów.
Na przykład miałem problem z testowaniem zdarzenia kliknięcia. Bez względu na to, czego próbowałem, po prostu nie zadzwoniłby do słuchacza. Potem zdałem sobie sprawę, że Jest wyśmiewany z funkcji i nigdy mi tego nie powiedział.
Ale najgorszą wadą Jest, jak dotąd, było to, że nie miał trybu zegarka, który automatycznie testowałby nowe zmiany. Moglibyśmy to raz uruchomić, uzyskać wyniki testów i to wszystko. (Lubię uruchamiać moje testy w tle podczas pracy. W przeciwnym razie zapominam je uruchomić.) W dzisiejszych czasach to już nie jest problem.
Aha, i Jest nie obsługuje uruchamiania testów React w wielu przeglądarkach. To mniejszy problem niż kiedyś, ale wydaje mi się, że jest to ważna funkcja w tej rzadkiej sytuacji, gdy heisenbug zdarza się tylko w określonej wersji Chrome…
Uwaga redaktora: Od czasu, gdy ten artykuł został pierwotnie napisany, Jest znacznie się poprawił. Możesz przeczytać nasz nowszy samouczek, React Unit Testing using Enzyme and Jest, i sam zdecydować, czy testowanie Jest w dzisiejszych czasach.
Testowanie reakcji: zintegrowany przykład
W każdym razie widzieliśmy, jak dobry test front-endowy React powinien działać w teorii. Wprowadźmy to w życie na krótkim przykładzie.
Zamierzamy zwizualizować różne sposoby generowania liczb losowych za pomocą komponentu wykresu rozrzutu stworzonego za pomocą Reacta i d3.js. Kod i jego demo są również na Github.
Użyjemy Karmy jako programu uruchamiającego testy, Mocha jako frameworka testowego, a Webpacka jako modułu ładującego.
Ustawić
Nasze pliki źródłowe zostaną umieszczone w <root>/src
, a testy umieścimy w <root>/src/__tests__
. Pomysł polega na tym, że możemy umieścić kilka katalogów w src
, po jednym dla każdego głównego komponentu i każdy z własnymi plikami testowymi. Łączenie kodu źródłowego i plików testowych w ten sposób ułatwia ponowne wykorzystanie komponentów React w różnych projektach.
Mając strukturę katalogów na miejscu, możemy zainstalować zależności takie jak:

$ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect
Jeśli coś się nie powiedzie, spróbuj ponownie uruchomić tę część instalacji. NPM czasami zawodzi w sposób, który znika przy ponownym uruchomieniu.
Po zakończeniu nasz plik package.json
powinien wyglądać tak:
// package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }
Po pewnej konfiguracji będziemy mogli uruchamiać testy za pomocą npm test
lub karma start
.
Konfiguracja
Niewiele jest w konfiguracji. Musimy upewnić się, że Webpack wie, jak znaleźć nasz kod, a Karma wie, jak przeprowadzać testy.
Umieściliśmy dwie linijki kodu JavaScript w pliku ./tests.webpack.js
, aby pomóc Karmie i Webpackowi grać razem:
// tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);
To mówi Webpackowi, aby uznał wszystko z przyrostkiem -test
za część zestawu testów.
Konfiguracja Karmy zajmuje trochę więcej pracy:
// karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };
Większość z tych linii pochodzi z domyślnej konfiguracji Karmy. Używaliśmy browsers
, aby powiedzieć, że testy powinny być uruchamiane w Chrome, frameworks
do określenia, z której platformy testowej korzystamy, a singleRun
, aby testy były domyślnie uruchamiane tylko raz. Możesz utrzymać karmę działającą w tle za pomocą karma start --no-single-run
.
Te trzy są oczywiste. Rzeczy z Webpack są bardziej interesujące.
Ponieważ Webpack obsługuje drzewo zależności naszego kodu, nie musimy określać wszystkich naszych plików w tablicy files
. Potrzebujemy tylko tests.webpack.js
, który następnie wymaga wszystkich niezbędnych plików.
Używamy ustawienia webpack
, aby powiedzieć Webpackowi, co ma robić. W normalnym środowisku ta część zostałaby umieszczona w pliku webpack.config.js
.
Mówimy również Webpackowi, aby używał babel-loader
dla naszych skryptów JavaScript. To daje nam wszystkie fantazyjne nowe funkcje z ECMAScript2015 i JSX Reacta.
W konfiguracji webpackServer
mówimy Webpackowi, aby nie drukował żadnych informacji debugowania. Zepsułoby to tylko nasz wynik testu.
Komponent reakcji i test
Z uruchomionym zestawem testów reszta jest prosta. Musimy stworzyć komponent, który akceptuje tablicę losowych współrzędnych i tworzy element <svg>
z mnóstwem punktów.
Zgodnie z najlepszymi praktykami testowania React — tj. standardową praktyką TDD — najpierw napiszemy test, a następnie właściwy komponent React. Zacznijmy od pliku waniliowych testów w src/__tests__/
:
// ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });
Najpierw potrzebujemy Reacta, jego TestUtils, d3.js, biblioteki expect
i testowanego kodu. Następnie tworzymy nowy zestaw testów z describe
i tworzymy losowe dane.
W naszym pierwszym teście upewnijmy się, że ScatterPlot
tytuł. Nasz test przechodzi do bloku describe
:
// ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });
Większość testów przebiega według tego samego schematu:
- Renderowanie.
- Znajdź konkretny węzeł.
- Sprawdź zawartość.
Jak widzieliśmy wcześniej, renderIntoDocument
renderuje nasz komponent, findRenderedDOMComponentWithTag
znajduje konkretną część, którą testujemy, a getDOMNode
daje nam surowy dostęp do DOM.
Na początku nasz test się nie powiedzie. Aby to przejść, musimy napisać komponent, który renderuje tag title:
var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;
Otóż to. Komponent ScatterPlot
renderuje <div>
ze znacznikiem <h1>
zawierającym oczekiwany tekst, a nasz test przejdzie. Tak, to więcej niż tylko HTML, ale wyrozumiałam się.
Narysuj resztę sowy
Możesz zobaczyć resztę naszego przykładu na GitHub, jak wspomniano powyżej. Pominiemy opisywanie tego krok po kroku w tym artykule, ale ogólny proces jest taki sam, jak powyżej. Ale chcę wam pokazać ciekawszy test. Test, który zapewnia, że wszystkie punkty danych pojawią się na wykresie:
// ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });
Taki jak wcześniej. Renderuj, znajdź węzły, sprawdź wynik. Interesującą częścią jest rysowanie tych węzłów DOM. Dodajemy trochę magii d3.js do komponentu ScatterPlot
w następujący sposób:
// ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...
Używamy componentWillMount
aby skonfigurować puste skale d3 dla domen X i Y , a componentWillReceiveProps
, aby upewnić się, że są aktualizowane, gdy coś się zmieni. Następnie update_d3
upewnia się, że ustawiłeś domain
i range
dla obu skal.
Użyjemy dwóch skal do przetłumaczenia między losowymi wartościami w naszym zbiorze danych a pozycjami na obrazie. Większość generatorów losowych zwraca liczby z zakresu [0,1] , który jest zbyt mały, aby można go było zobaczyć jako piksele.
Następnie dodajemy punkty do metody render naszego komponentu:
// ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }
Ten kod przechodzi przez tablicę this.props.data
i dodaje element <circle>
dla każdego punktu danych. Prosty.
Jeśli chcesz dowiedzieć się więcej o łączeniu Reacta i d3.js w celu tworzenia komponentów wizualizacji danych, to kolejny świetny powód, aby zajrzeć do mojej książki React+d3.js .
Zautomatyzowane testowanie komponentów React: łatwiejsze niż się wydaje
To wszystko, co musimy wiedzieć o pisaniu testowalnych komponentów front-endowych za pomocą Reacta. Aby zobaczyć więcej testujących kodu komponentów React, sprawdź przykładową bazę kodu React na Github, jak wspomniano powyżej.
Dowiedzieliśmy się, że:
- React zmusza nas do modularyzacji i hermetyzacji.
- To sprawia, że testowanie React UI jest łatwe do zautomatyzowania.
- Testy jednostkowe nie wystarczą dla front-endów.
- Karma jest świetnym biegaczem testowym.
- Jest ma potencjał, ale jeszcze go nie ma. (A może teraz jest.)
Jeśli podobał Ci się ten artykuł, śledź mnie na Twitterze i zostaw komentarz poniżej. Dziękujemy za przeczytanie i życzę miłego testowania React!