Pisanie testowalnego kodu w JavaScript: krótki przegląd
Opublikowany: 2022-03-11Niezależnie od tego, czy używamy Node w połączeniu z frameworkiem testowym, takim jak Mocha lub Jasmine, czy uruchamiamy testy zależne od DOM w bezgłowej przeglądarce, takiej jak PhantomJS, nasze opcje testowania jednostkowego JavaScript są teraz lepsze niż kiedykolwiek.
Nie oznacza to jednak, że testowany przez nas kod jest dla nas tak łatwy jak nasze narzędzia! Organizowanie i pisanie kodu, który można łatwo przetestować, wymaga trochę wysiłku i planowania, ale istnieje kilka wzorców, inspirowanych koncepcjami programowania funkcjonalnego, których możemy użyć, aby uniknąć wpadnięcia w trudne miejsce, gdy nadchodzi czas testowania naszego kodu. W tym artykule omówimy kilka przydatnych wskazówek i wzorców dotyczących pisania testowalnego kodu w JavaScript.
Oddziel logikę biznesową od logiki wyświetlania
Jednym z podstawowych zadań aplikacji przeglądarki opartej na języku JavaScript jest nasłuchiwanie zdarzeń DOM wyzwalanych przez użytkownika końcowego, a następnie reagowanie na nie poprzez uruchomienie logiki biznesowej i wyświetlenie wyników na stronie. Kuszące jest napisanie anonimowej funkcji, która wykonuje większość pracy właśnie tam, gdzie konfigurujesz detektory zdarzeń DOM. Problem, który to stwarza, polega na tym, że musisz teraz symulować zdarzenia DOM, aby przetestować swoją anonimową funkcję. Może to spowodować narzut zarówno w wierszach kodu, jak i czasie potrzebnym na uruchomienie testów.
Zamiast tego napisz nazwaną funkcję i przekaż ją do obsługi zdarzeń. W ten sposób możesz pisać testy dla nazwanych funkcji bezpośrednio i bez przeskakiwania przez obręcze w celu wywołania fałszywego zdarzenia DOM.
Dotyczy to jednak nie tylko DOM. Wiele interfejsów API, zarówno w przeglądarce, jak i w węźle, jest zaprojektowanych tak, aby uruchamiać i nasłuchiwać zdarzenia lub czekać na zakończenie innych rodzajów pracy asynchronicznej. Ogólna zasada jest taka, że jeśli piszesz wiele anonimowych funkcji wywołań zwrotnych, Twój kod może nie być łatwy do przetestowania.
// hard to test $('button').on('click', () => { $.getJSON('/path/to/data') .then(data => { $('#my-list').html('results: ' + data.join(', ')); }); }); // testable; we can directly run fetchThings to see if it // makes an AJAX request without having to trigger DOM // events, and we can run showThings directly to see that it // displays data in the DOM without doing an AJAX request $('button').on('click', () => fetchThings(showThings)); function fetchThings(callback) { $.getJSON('/path/to/data').then(callback); } function showThings(data) { $('#my-list').html('results: ' + data.join(', ')); }
Użyj wywołań zwrotnych lub obietnic z kodem asynchronicznym
W powyższym przykładzie kodu nasza zrefaktoryzowana funkcja fetchThings uruchamia żądanie AJAX, które większość swojej pracy wykonuje asynchronicznie. Oznacza to, że nie możemy uruchomić funkcji i sprawdzić, czy wykonała wszystko, czego oczekiwaliśmy, ponieważ nie będziemy wiedzieć, kiedy zakończy działanie.
Najczęstszym sposobem rozwiązania tego problemu jest przekazanie funkcji zwrotnej jako parametru funkcji działającej asynchronicznie. W testach jednostkowych możesz uruchamiać swoje asercje w przekazanym wywołaniu zwrotnym.
Innym powszechnym i coraz bardziej popularnym sposobem organizowania kodu asynchronicznego jest interfejs API Promise. Na szczęście $.ajax i większość innych funkcji asynchronicznych jQuery już zwracają obiekt Promise, więc omówiono już wiele typowych przypadków użycia.
// hard to test; we don't know how long the AJAX request will run function fetchData() { $.ajax({ url: '/path/to/data' }); } // testable; we can pass a callback and run assertions inside it function fetchDataWithCallback(callback) { $.ajax({ url: '/path/to/data', success: callback, }); } // also testable; we can run assertions when the returned Promise resolves function fetchDataWithPromise() { return $.ajax({ url: '/path/to/data' }); }
Unikaj skutków ubocznych
Napisz funkcje, które pobierają argumenty i zwracają wartość opartą wyłącznie na tych argumentach, podobnie jak wybijanie liczb w równaniu matematycznym w celu uzyskania wyniku. Jeśli twoja funkcja zależy od jakiegoś zewnętrznego stanu (na przykład właściwości instancji klasy lub zawartości pliku) i musisz ustawić ten stan przed testowaniem swojej funkcji, musisz wykonać więcej ustawień w swoich testach. Musisz ufać, że żaden inny uruchamiany kod nie zmienia tego samego stanu.
W tym samym duchu unikaj pisania funkcji, które zmieniają stan zewnętrzny (takich jak zapisywanie do pliku lub zapisywanie wartości w bazie danych) podczas jego działania. Zapobiega to skutkom ubocznym, które mogłyby wpłynąć na Twoją zdolność do testowania innego kodu z ufnością. Ogólnie rzecz biorąc, najlepiej jest trzymać efekty uboczne jak najbliżej krawędzi kodu, z jak najmniejszym „obszarem powierzchni”. W przypadku klas i instancji obiektów skutki uboczne metody klasy powinny być ograniczone do stanu testowanej instancji klasy.
// hard to test; we have to set up a globalListOfCars object and set up a // DOM with a #list-of-models node to test this code function processCarData() { const models = globalListOfCars.map(car => car.model); $('#list-of-models').html(models.join(', ')); } // easy to test; we can pass an argument and test its return value, without // setting any global values on the window or checking the DOM the result function buildModelsString(cars) { const models = cars.map(car => car.model); return models.join(','); }
Użyj wstrzykiwania zależności
Jednym z typowych wzorców ograniczania użycia przez funkcję stanu zewnętrznego jest wstrzykiwanie zależności — przekazywanie wszystkich zewnętrznych potrzeb funkcji jako parametrów funkcji.

// depends on an external state database connector instance; hard to test function updateRow(rowId, data) { myGlobalDatabaseConnector.update(rowId, data); } // takes a database connector instance in as an argument; easy to test! function updateRow(rowId, data, databaseConnector) { databaseConnector.update(rowId, data); }
Jedną z głównych korzyści płynących z wstrzykiwania zależności jest to, że możesz przekazywać pozorne obiekty z testów jednostkowych, które nie powodują rzeczywistych skutków ubocznych (w tym przypadku aktualizowanie wierszy bazy danych) i możesz po prostu stwierdzić, że twój model został zastosowany w oczekiwany sposób.
Nadaj każdej funkcji jeden cel
Podziel długie funkcje, które wykonują kilka czynności, na zbiór krótkich funkcji o jednym przeznaczeniu. To znacznie ułatwia testowanie, czy każda funkcja wykonuje swoją część poprawnie, zamiast mieć nadzieję, że duża funkcja zrobi wszystko poprawnie przed zwróceniem wartości.
W programowaniu funkcjonalnym czynność łączenia kilku funkcji jednofunkcyjnych razem nazywana jest kompozycją. Underscore.js ma nawet funkcję _.compose
, która pobiera listę funkcji i łączy je ze sobą, pobierając wartość zwracaną z każdego kroku i przekazując ją do następnej funkcji w wierszu.
// hard to test function createGreeting(name, location, age) { let greeting; if (location === 'Mexico') { greeting = '!Hola'; } else { greeting = 'Hello'; } greeting += ' ' + name.toUpperCase() + '! '; greeting += 'You are ' + age + ' years old.'; return greeting; } // easy to test function getBeginning(location) { if (location === 'Mexico') { return 'Hola'; } else { return 'Hello'; } } function getMiddle(name) { return ' ' + name.toUpperCase() + '! '; } function getEnd(age) { return 'You are ' + age + ' years old.'; } function createGreeting(name, location, age) { return getBeginning(location) + getMiddle(name) + getEnd(age); }
Nie mutuj parametrów
W JavaScript tablice i obiekty są przekazywane przez referencję, a nie przez wartość, i są one zmienne. Oznacza to, że gdy przekazujesz obiekt lub tablicę jako parametr do funkcji, zarówno Twój kod, jak i funkcja, którą przekazałeś obiekt lub tablicę, mają możliwość zmiany tego samego wystąpienia tej tablicy lub obiektu w pamięci. Oznacza to, że jeśli testujesz własny kod, musisz ufać, że żadna z funkcji, które wywołasz w kodzie, nie zmieni twoich obiektów. Za każdym razem, gdy dodajesz w kodzie nowe miejsce, które zmienia ten sam obiekt, coraz trudniej jest śledzić, jak ten obiekt powinien wyglądać, co utrudnia testowanie.
Zamiast tego, jeśli masz funkcję, która pobiera obiekt lub tablicę, powinna działać na tym obiekcie lub tablicy tak, jakby była tylko do odczytu. Utwórz nowy obiekt lub tablicę w kodzie i dodaj do niego wartości w zależności od potrzeb. Lub użyj podkreślenia lub Lodash, aby sklonować przekazany obiekt lub tablicę przed wykonaniem na nim operacji. Co więcej, użyj narzędzia takiego jak Immutable.js, które tworzy struktury danych tylko do odczytu.
// alters objects passed to it function upperCaseLocation(customerInfo) { customerInfo.location = customerInfo.location.toUpperCase(); return customerInfo; } // sends a new object back instead function upperCaseLocation(customerInfo) { return { name: customerInfo.name, location: customerInfo.location.toUpperCase(), age: customerInfo.age }; }
Napisz swoje testy przed kodem
Proces pisania testów jednostkowych przed testowanym kodem nazywa się programowaniem opartym na testach (TDD). Wielu programistów uważa TDD za bardzo pomocne.
Pisząc najpierw swoje testy, jesteś zmuszony pomyśleć o interfejsie API, który udostępniasz, z perspektywy konsumującego go programisty. Pomaga również upewnić się, że piszesz tylko tyle kodu, aby spełnić warunki umowy, które są wymuszane przez twoje testy, zamiast przeprojektowywać rozwiązanie, które jest niepotrzebnie złożone.
W praktyce TDD jest dyscypliną, której zobowiązanie do przestrzegania wszystkich zmian w kodzie może być trudne. Ale kiedy wydaje się, że warto spróbować, jest to świetny sposób, aby zagwarantować, że cały kod będzie możliwy do przetestowania.
Zakończyć
Wszyscy wiemy, że istnieje kilka pułapek, na które bardzo łatwo można wpaść podczas pisania i testowania złożonych aplikacji JavaScript. Ale miejmy nadzieję, że dzięki tym wskazówkom i pamiętając o tym, aby zawsze utrzymywać nasz kod tak prosty i funkcjonalny, jak to tylko możliwe, możemy utrzymać wysoki poziom pokrycia testami i niską ogólną złożoność kodu!
- 10 najczęstszych błędów popełnianych przez programistów JavaScript
- Need for Speed: Topowa retrospektywa wyzwań związanych z kodowaniem JavaScript