Scrierea codului testabil în JavaScript: o scurtă prezentare generală

Publicat: 2022-03-11

Indiferent dacă folosim Node împreună cu un cadru de testare precum Mocha sau Jasmine, fie că realizăm teste dependente de DOM într-un browser fără cap precum PhantomJS, opțiunile noastre pentru testarea unitară JavaScript sunt mai bune acum ca niciodată.

Cu toate acestea, acest lucru nu înseamnă că codul pe care îl testăm este la fel de ușor pentru noi precum instrumentele noastre! Organizarea și scrierea unui cod care poate fi testat cu ușurință necesită ceva efort și planificare, dar există câteva modele, inspirate din conceptele de programare funcțională, pe care le putem folosi pentru a evita să intrăm într-un punct dificil atunci când vine timpul să ne testăm codul. În acest articol, vom parcurge câteva sfaturi și modele utile pentru scrierea codului testabil în JavaScript.

Păstrați logica de afaceri și logica de afișare separate

Una dintre sarcinile principale ale unei aplicații de browser bazate pe JavaScript este să asculte evenimentele DOM declanșate de utilizatorul final și apoi să răspundă la ele rulând o logică de afaceri și afișând rezultatele pe pagină. Este tentant să scrieți o funcție anonimă care face cea mai mare parte a muncii chiar acolo unde vă configurați ascultătorii de evenimente DOM. Problema pe care o creează aceasta este că acum trebuie să simulați evenimentele DOM pentru a vă testa funcția anonimă. Acest lucru poate crea supraîncărcare atât în ​​linii de cod, cât și în timpul necesar pentru rularea testelor.

În schimb, scrieți o funcție numită și transmiteți-o handler-ului de evenimente. În acest fel, puteți scrie teste pentru funcțiile numite direct și fără a sări prin cercuri pentru a declanșa un eveniment DOM fals.

Totuși, acest lucru se aplică mai mult decât DOM. Multe API-uri, atât în ​​browser, cât și în Node, sunt concepute pentru declanșarea și ascultarea evenimentelor sau așteptarea finalizării altor tipuri de lucrări asincrone. O regulă generală este că, dacă scrieți o mulțime de funcții de apel invers anonim, codul dvs. poate să nu fie ușor de testat.

 // 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(', ')); }

Utilizați apeluri inverse sau promisiuni cu cod asincron

În exemplul de cod de mai sus, funcția noastră refactorizată fetchThings rulează o solicitare AJAX, care își desfășoară cea mai mare parte a activității asincron. Aceasta înseamnă că nu putem rula funcția și nu putem testa că a făcut tot ce ne așteptam, pentru că nu vom ști când s-a terminat de rulat.

Cea mai comună modalitate de a rezolva această problemă este de a trece o funcție de apel invers ca parametru funcției care rulează asincron. În testele unitare, vă puteți rula afirmațiile în apelul invers pe care îl treceți.

Ilustrație: Utilizarea unei uncțiuni de apel invers ca parametru în testarea unitară

Un alt mod comun și din ce în ce mai popular de a organiza codul asincron este cu API-ul Promise. Din fericire, $.ajax și majoritatea celorlalte funcții asincrone ale jQuery returnează deja un obiect Promise, așa că o mulțime de cazuri de utilizare obișnuite sunt deja acoperite.

 // 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' }); }

Evitați efectele secundare

Scrieți funcții care preiau argumente și returnează o valoare bazată exclusiv pe acele argumente, la fel ca introducerea numerelor într-o ecuație matematică pentru a obține un rezultat. Dacă funcția dvs. depinde de o stare externă (proprietățile unei instanțe de clasă sau conținutul unui fișier, de exemplu) și trebuie să configurați acea stare înainte de a vă testa funcția, trebuie să faceți mai multe setări în teste. Va trebui să aveți încredere că orice alt cod rulat nu modifică aceeași stare.

Ilustrație: efect în cascadă cauzat de starea externă.

În același sens, evitați să scrieți funcții care modifică starea externă (cum ar fi scrierea într-un fișier sau salvarea valorilor într-o bază de date) în timp ce rulează. Acest lucru previne efectele secundare care v-ar putea afecta capacitatea de a testa alt cod cu încredere. În general, cel mai bine este să păstrați efectele secundare cât mai aproape de marginile codului, cu cât mai puțină „suprafață” posibil. În cazul claselor și al instanțelor de obiect, efectele secundare ale unei metode de clasă ar trebui limitate la starea instanței de clasă testată.

 // 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(','); }

Utilizați injecția de dependență

Un model comun pentru reducerea utilizării unei stări externe de către o funcție este injecția de dependență - trecerea tuturor nevoilor externe ale unei funcții ca parametri ai funcției.

 // 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); }

Unul dintre principalele avantaje ale utilizării injecției de dependență este că puteți trece obiecte simulate din testele dvs. unitare care nu provoacă efecte secundare reale (în acest caz, actualizarea rândurilor bazei de date) și puteți afirma doar că obiectul dvs. simulat a fost acționat asupra în modul aşteptat.

Dați fiecărei funcție un singur scop

Împărțiți funcțiile lungi care fac mai multe lucruri într-o colecție de funcții scurte, cu un singur scop. Acest lucru face mult mai ușor să testați că fiecare funcție își face partea corect, mai degrabă decât să sperați că una mare face totul corect înainte de a returna o valoare.

În programarea funcțională, actul de a înșira mai multe funcții cu un singur scop împreună se numește compoziție. Underscore.js are chiar și o funcție _.compose , care preia o listă de funcții și le conectează împreună, luând valoarea returnată a fiecărui pas și trecând-o la următoarea funcție din linie.

 // 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); }

Nu modificați parametrii

În JavaScript, matricele și obiectele sunt transmise mai degrabă prin referință decât prin valoare și sunt modificabile. Aceasta înseamnă că atunci când treceți un obiect sau o matrice ca parametru într-o funcție, atât codul dvs., cât și funcția pe care ați transmis obiectul sau matricea, au capacitatea de a modifica aceeași instanță a acelei matrice sau obiect în memorie. Aceasta înseamnă că, dacă vă testați propriul cod, trebuie să aveți încredere că niciuna dintre funcțiile pe care le apelează codul nu vă modifică obiectele. De fiecare dată când adăugați un loc nou în codul dvs. care modifică același obiect, devine din ce în ce mai greu să urmăriți cum ar trebui să arate acel obiect, ceea ce face mai greu de testat.

Ilustrație: mutarea parametrilor poate cauza probleme

În schimb, dacă aveți o funcție care preia un obiect sau o matrice, acționați asupra aceluiași obiect sau matrice ca și cum ar fi doar pentru citire. Creați un nou obiect sau o matrice în cod și adăugați-i valori în funcție de nevoile dvs. Sau, utilizați Underscore sau Lodash pentru a clona obiectul sau matricea transmisă înainte de a opera pe el. Și mai bine, utilizați un instrument precum Immutable.js care creează structuri de date doar pentru citire.

 // 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 }; }

Scrieți testele înainte de cod

Procesul de scriere a testelor unitare înainte de codul pe care îl testează se numește dezvoltare condusă de teste (TDD). Mulți dezvoltatori consideră că TDD este foarte util.

Scriind mai întâi testele, ești forțat să te gândești la API-ul pe care îl expui din perspectiva unui dezvoltator care îl consumă. De asemenea, vă ajută să vă asigurați că scrieți doar suficient cod pentru a îndeplini contractul impus de testele dvs., mai degrabă decât să creați în exces o soluție care este inutil de complexă.

În practică, TDD este o disciplină la care poate fi dificil să te angajezi pentru toate modificările de cod. Dar când pare că merită încercat, este o modalitate excelentă de a vă garanta că păstrați tot codul testabil.

Învelire

Știm cu toții că există câteva capcane care sunt foarte ușor de acceptat atunci când scrieți și testați aplicații JavaScript complexe. Dar sperăm că, cu aceste sfaturi și amintindu-ne să păstrăm întotdeauna codul cât mai simplu și funcțional posibil, putem menține acoperirea testului ridicată și complexitatea generală a codului scăzută!

Legate de:
  • Cele mai frecvente 10 greșeli pe care le fac dezvoltatorii JavaScript
  • Nevoia de viteză: o retrospectivă a provocării Toptal de codare JavaScript