Un ghid Node.js pentru a face efectiv teste de integrare

Publicat: 2022-03-11

Testele de integrare nu sunt ceva de care ar trebui de temut. Ele sunt o parte esențială a testării complete a aplicației dvs.

Când vorbim despre testare, de obicei ne gândim la teste unitare în care testăm o mică bucată de cod izolat. Cu toate acestea, aplicația dvs. este mai mare decât acea mică bucată de cod și aproape nicio parte a aplicației dvs. nu funcționează izolat. Aici testele de integrare își dovedesc importanța. Testele de integrare pornesc acolo unde testele unitare sunt insuficiente și reduc decalajul dintre testele unitare și testele end-to-end.

Știți că trebuie să scrieți teste de integrare, așa că de ce nu o faceți?
Tweet

În acest articol, veți învăța cum să scrieți teste de integrare care pot fi citite și compuse cu exemple în aplicații bazate pe API.

Deși vom folosi JavaScript/Node.js pentru toate exemplele de cod din acest articol, majoritatea ideilor discutate pot fi adaptate cu ușurință la testele de integrare pe orice platformă.

Teste unitare vs teste de integrare: aveți nevoie de ambele

Testele unitare se concentrează pe o anumită unitate de cod. Adesea, aceasta este o metodă specifică sau o funcție a unei componente mai mari.

Aceste teste sunt efectuate izolat, în cazul în care toate dependențele externe sunt de obicei blocate sau batjocorite.

Cu alte cuvinte, dependențele sunt înlocuite cu un comportament preprogramat, asigurându-se că rezultatul testului este determinat doar de corectitudinea unității testate.

Puteți afla mai multe despre testele unitare aici.

Testele unitare sunt folosite pentru a menține codul de înaltă calitate, cu un design bun. De asemenea, ne permit să acoperim cu ușurință carcasele de colț.

Dezavantajul este că testele unitare nu pot acoperi interacțiunea dintre componente. Aici devin utile testele de integrare.

Teste de integrare

Dacă testele unitare sunt definite prin testarea celor mai mici unități de cod în mod izolat, atunci testele de integrare sunt exact opusul.

Testele de integrare sunt folosite pentru a testa mai multe unități (componente) mai mari în interacțiune și, uneori, pot acoperi mai multe sisteme.

Scopul testelor de integrare este de a găsi erori în conexiunile și dependențele dintre diferite componente, cum ar fi:

  • Transmiterea de argumente nevalide sau ordonate incorect
  • Schema bazei de date rupte
  • Integrare cache nevalidă
  • Defecte în logica de afaceri sau erori în fluxul de date (deoarece testarea se face acum dintr-o perspectivă mai largă).

Dacă componentele pe care le testăm nu au nicio logică complicată (de exemplu, componente cu complexitate ciclomatică minimă), testele de integrare vor fi mult mai importante decât testele unitare.

În acest caz, testele unitare vor fi utilizate în primul rând pentru a impune un design bun de cod.

În timp ce testele unitare ajută la asigurarea faptului că funcțiile sunt scrise corect, testele de integrare ajută la asigurarea faptului că sistemul funcționează corect ca întreg. Deci, atât testele unitare, cât și testele de integrare își servesc fiecare propriul scop complementar și ambele sunt esențiale pentru o abordare cuprinzătoare de testare.

Testele unitare și testele de integrare sunt ca două fețe ale aceleiași monede. Moneda nu este valabilă fără ambele.

Prin urmare, testarea nu este completă până când nu finalizați atât testele de integrare, cât și testele unitare.

Configurați suita pentru teste de integrare

În timp ce configurarea unei suite de teste pentru testele unitare este destul de simplă, configurarea unei suite de teste pentru testele de integrare este adesea mai dificilă.

De exemplu, componentele din testele de integrare pot avea dependențe care se află în afara proiectului, cum ar fi baze de date, sisteme de fișiere, furnizori de e-mail, servicii de plată externe și așa mai departe.

Ocazional, testele de integrare trebuie să utilizeze aceste servicii și componente externe și, uneori, pot fi blocate.

Când sunt necesare, poate duce la mai multe provocări.

  • Execuție fragilă a testului: serviciile externe pot fi indisponibile, pot returna un răspuns nevalid sau pot fi într-o stare invalidă. În unele cazuri, acest lucru poate duce la un fals pozitiv, alteori poate duce la un fals negativ.
  • Execuție lentă: pregătirea și conectarea la servicii externe poate fi lentă. De obicei, testele sunt executate pe un server extern ca parte a CI.
  • Configurare complexă de testare: serviciile externe trebuie să fie în starea dorită pentru testare. De exemplu, baza de date ar trebui să fie preîncărcată cu datele de testare necesare etc.

Instrucțiuni de urmat în timpul scrierii testelor de integrare

Testele de integrare nu au reguli stricte precum testele unitare. În ciuda acestui fapt, există câteva instrucțiuni generale de urmat atunci când scrieți teste de integrare.

Teste repetabile

Ordinea testului sau dependențele nu ar trebui să modifice rezultatul testului. Executarea aceluiași test de mai multe ori ar trebui să returneze întotdeauna același rezultat. Acest lucru poate fi dificil de realizat dacă testul folosește Internetul pentru a se conecta la servicii terțe. Cu toate acestea, această problemă poate fi rezolvată prin împodobire și batjocură.

Pentru dependențele externe asupra cărora aveți mai mult control, configurarea pașilor înainte și după un test de integrare vă va ajuta să vă asigurați că testul este întotdeauna rulat pornind dintr-o stare identică.

Testarea acțiunilor relevante

Pentru a testa toate cazurile posibile, testele unitare sunt o opțiune mult mai bună.

Testele de integrare sunt mai orientate pe conexiunea dintre module, prin urmare testarea scenariilor fericite este de obicei calea de urmat, deoarece va acoperi conexiunile importante dintre module.

Test și afirmație inteligibilă

O vizualizare rapidă a testului ar trebui să informeze cititorul ce este testat, cum este configurat mediul, ce este blocat, când este executat testul și ce este afirmat. Aserțiunile ar trebui să fie simple și să folosească ajutoare pentru o mai bună comparare și înregistrare.

Configurare ușoară a testului

Aducerea testului la starea inițială ar trebui să fie cât mai simplă și cât mai ușor de înțeles.

Evitați testarea codului terților

În timp ce serviciile terță parte pot fi utilizate în teste, nu este nevoie să le testați. Și dacă nu ai încredere în ele, probabil că nu ar trebui să le folosești.

Lăsați codul de producție liber de codul de testare

Codul de producție ar trebui să fie curat și simplu. Amestecarea codului de testare cu codul de producție va duce la cuplarea a două domenii neconectabile.

Jurnalul relevant

Testele eșuate nu sunt foarte valoroase fără o înregistrare bună.

Când testele trec, nu este nevoie de înregistrare suplimentară. Dar atunci când eșuează, înregistrarea extinsă este vitală.

Jurnalul ar trebui să conțină toate interogările bazei de date, solicitările și răspunsurile API, precum și o comparație completă a ceea ce este afirmat. Acest lucru poate facilita semnificativ depanarea.

Testele bune arată curate și inteligibile

Un test simplu care urmează instrucțiunile de aici ar putea arăta astfel:

 const co = require('co'); const test = require('blue-tape'); const factory = require('factory'); const superTest = require('../utils/super_test'); const testEnvironment = require('../utils/test_environment_preparer'); const path = '/v1/admin/recipes'; test(`API GET ${path}`, co.wrap(function* (t) { yield testEnvironment.prepare(); const recipe1 = yield factory.create('recipe'); const recipe2 = yield factory.create('recipe'); const serverResponse = yield superTest.get(path); t.deepEqual(serverResponse.body, [recipe1, recipe2]); }));

Codul de mai sus testează un API ( GET /v1/admin/recipes ) care se așteaptă ca acesta să returneze o serie de rețete salvate ca răspuns.

Puteți vedea că testul, oricât de simplu ar fi, se bazează pe o mulțime de utilități. Acest lucru este comun pentru orice suită de teste de integrare bună.

Componentele de ajutor facilitează scrierea de teste de integrare inteligibile.

Să analizăm ce componente sunt necesare pentru testarea integrării.

Componente de ajutor

O suită cuprinzătoare de testare are câteva ingrediente de bază, inclusiv: controlul fluxului, cadrul de testare, gestionarea bazei de date și o modalitate de a vă conecta la API-urile backend.

Controlul debitului

Una dintre cele mai mari provocări în testarea JavaScript este fluxul asincron.

Reapelurile pot face ravagii în cod și promisiunile pur și simplu nu sunt suficiente. Acesta este locul în care ajutoarele de flux devin utile.

În timp ce așteptați ca async/wait să fie pe deplin acceptat, pot fi utilizate biblioteci cu comportament similar. Scopul este de a scrie cod lizibil, expresiv și robust, cu posibilitatea de a avea flux asincron.

Co permite ca codul să fie scris într-un mod frumos, în timp ce îl menține neblocant. Acest lucru se realizează prin definirea unei funcții de cogenerator și apoi obținerea rezultatelor.

O altă soluție este să folosești Bluebird. Bluebird este o bibliotecă de promisiune care are caracteristici foarte utile, cum ar fi gestionarea matricelor, erorilor, timpului etc.

Coroutine Co și Bluebird se comportă în mod similar cu asincron/așteaptă în ES7 (se așteaptă rezoluția înainte de a continua), singura diferență fiind că va returna întotdeauna o promisiune, care este utilă pentru gestionarea erorilor.

Cadrul de testare

Alegerea unui cadru de testare se reduce doar la preferințele personale. Preferința mea este un cadru care este ușor de utilizat, nu are efecte secundare și care este ușor de citit și de canalizat.

Există o gamă largă de cadre de testare în JavaScript. În exemplele noastre, folosim Tape. Banda, în opinia mea, nu numai că îndeplinește aceste cerințe, dar este și mai curată și mai simplă decât alte cadre de testare precum Mocha sau Jasmin.

Banda se bazează pe protocolul Test Anything (TAP).

TAP are variante pentru majoritatea limbajelor de programare.

Tape preia testele ca intrare, le rulează și apoi scoate rezultatele ca TAP. Rezultatul TAP poate fi apoi transmis către raportorul de testare sau poate fi transmis către consolă într-un format brut. Banda este rulată din linia de comandă.

Tape are câteva caracteristici frumoase, cum ar fi definirea unui modul de încărcat înainte de a rula întreaga suită de testare, oferirea unei biblioteci de aserții mici și simple și definirea numărului de aserțiuni care ar trebui apelate într-un test. Utilizarea unui modul pentru preîncărcare poate simplifica pregătirea unui mediu de testare și poate elimina orice cod inutil.

Biblioteca fabricii

O bibliotecă din fabrică vă permite să înlocuiți fișierele de fixare statice cu o modalitate mult mai flexibilă de a genera date pentru un test. O astfel de bibliotecă vă permite să definiți modele și să creați entități pentru acele modele fără a scrie cod dezordonat și complex.

JavaScript are factory_girl pentru aceasta - o bibliotecă inspirată dintr-o bijuterie cu un nume similar, care a fost dezvoltată inițial pentru Ruby on Rails.

 const factory = require('factory-girl').factory; const User = require('../models/user'); factory.define('user', User, { username: 'Bob', number_of_recipes: 50 }); const user = factory.build('user');

Pentru a începe, un nou model trebuie definit în factory_girl.

Este specificat cu un nume, un model din proiectul dvs. și un obiect din care este generată o nouă instanță.

Alternativ, în loc să definiți obiectul din care este generată o nouă instanță, poate fi furnizată o funcție care va returna un obiect sau o promisiune.

Când creăm o nouă instanță a unui model, putem:

  • Ignorați orice valoare din instanța nou generată
  • Transmiteți valori suplimentare opțiunii funcție de compilare

Să vedem un exemplu.

 const factory = require('factory-girl').factory; const User = require('../models/user'); factory.define('user', User, (buildOptions) => { return { name: 'Mike', surname: 'Dow', email: buildOptions.email || '[email protected]' } }); const user1 = factory.build('user'); // {"name": "Mike", "surname": "Dow", "email": "[email protected]"} const user2 = factory.build('user', {name: 'John'}, {email: '[email protected]'}); // {"name": "John", "surname": "Dow", "email": "[email protected]"}

Conectarea la API-uri

Pornirea unui server HTTP complet și efectuarea unei solicitări HTTP efective, doar pentru a o demola câteva secunde mai târziu – în special atunci când se efectuează mai multe teste – este total ineficientă și poate determina testele de integrare să dureze mult mai mult decât este necesar.

SuperTest este o bibliotecă JavaScript pentru apelarea API-urilor fără a crea un nou server activ. Se bazează pe SuperAgent, o bibliotecă pentru crearea cererilor TCP. Cu această bibliotecă, nu este nevoie să creați noi conexiuni TCP. API-urile sunt apelate aproape instantaneu.

SuperTest, cu suport pentru promisiuni, este supertest-așa cum a promis. Când o astfel de solicitare returnează o promisiune, vă permite să evitați mai multe funcții de apel invers imbricate, ceea ce face mult mai ușor să gestionați fluxul.

 const express = require('express') const request = require('supertest-as-promised'); const app = express(); request(app).get("/recipes").then(res => assert(....));

SuperTest a fost realizat pentru cadrul Express.js, dar cu mici modificări poate fi folosit și cu alte cadre.

Alte Utilități

În unele cazuri, este necesar să ne batem joc de dependența din codul nostru, să testăm logica în jurul funcțiilor folosind spioni sau să folosim stub-uri în anumite locuri. Aici sunt utile unele dintre aceste pachete de utilitate.

SinonJS este o bibliotecă grozavă care acceptă spioni, stub-uri și batjocuri pentru teste. De asemenea, acceptă și alte funcții utile de testare, cum ar fi timpul de îndoire, sandbox de testare și afirmarea extinsă, precum și servere și solicitări false.

În unele cazuri, este necesar să ne batem joc de anumite dependențe în codul nostru. Referințele la servicii pe care am dori să le batem joc sunt folosite de alte părți ale sistemului.

Pentru a rezolva această problemă, putem folosi injecția de dependență sau, dacă aceasta nu este o opțiune, putem folosi un serviciu de batjocură precum Mockery.

Batjocorirea ajută la batjocorirea codului care are dependențe externe. Pentru a-l folosi corect, Mockery trebuie apelat înainte de a încărca testele sau codul.

 const mockery = require('mockery'); mockery.enable({ warnOnReplace: false, warnOnUnregistered: false }); const mockingStripe = require('lib/services/internal/stripe'); mockery.registerMock('lib/services/internal/stripe', mockingStripe);

Cu această nouă referință (în acest exemplu, mockingStripe ), este mai ușor să batem joc de servicii mai târziu în testele noastre.

 const stubStripeTransfer = sinon.stub(mockingStripe, 'transferAmount'); stubStripeTransfer.returns(Promise.resolve(null));

Cu ajutorul bibliotecii Sinon, este ușor de batjocorit. Singura problemă aici este că acest stub se va propaga la alte teste. Pentru a-l sandbox, se poate folosi cutia de nisip Sinon. Cu el, testele ulterioare pot aduce sistemul înapoi la starea inițială.

 const sandbox = require('sinon').sandbox.create(); const stubStripeTransfer = sandbox.sinon.stub(mockingStripe, 'transferAmount'); stubStripeTransfer.returns(Promise.resolve(null)); // after the test, or better when starting a new test sandbox.restore();

Este nevoie de alte componente pentru funcții precum:

  • Golirea bazei de date (se poate face cu o singură interogare ierarhică pre-build)
  • Setarea lui la starea de lucru (sequelize-fixtures)
  • Batjocorirea solicitărilor TCP către servicii terță parte (nock)
  • Folosind afirmații mai bogate (chai)
  • Răspunsuri salvate de la terți (remediere ușoară)

Teste nu atât de simple

Abstracția și extensibilitatea sunt elemente cheie pentru construirea unei suite de teste de integrare eficiente. Tot ceea ce îndepărtează focusul din nucleul testului (pregătirea datelor, acțiunea și afirmația acestuia) ar trebui grupat și rezumat în funcții de utilitate.

Deși nu există o cale corectă sau greșită aici, deoarece totul depinde de proiect și de nevoile acestuia, unele calități cheie sunt încă comune oricărei suită de teste de integrare bună.

Următorul cod arată cum să testați un API care creează o rețetă și trimite un e-mail ca efect secundar.

Acesta blochează furnizorul extern de e-mail, astfel încât să puteți testa dacă un e-mail ar fi fost trimis fără a trimite efectiv unul. Testul verifică, de asemenea, dacă API-ul a răspuns cu codul de stare corespunzător.

 const co = require('co'); const factory = require('factory'); const superTest = require('../utils/super_test'); const basicEnv = require('../utils/basic_test_enivornment'); const path = '/v1/admin/recipes'; basicEnv.test(`API POST ${path}`, co.wrap(function* (t, assert, sandbox) { const chef = yield factory.create('chef'); const body = { chef_id: chef.id, recipe_name: 'cake', Ingredients: ['carrot', 'chocolate', 'biscuit'] }; const stub = sandbox.stub(mockery.emailProvider, 'sendNewEmail').returnsPromise(null); const serverResponse = yield superTest.get(path, body); assert.spies(stub).called(1); assert.statusCode(serverResponse, 201); }));

Testul de mai sus este repetabil, deoarece începe cu un mediu curat de fiecare dată.

Are un proces simplu de configurare, în care tot ceea ce este legat de configurare este consolidat în cadrul funcției basicEnv.test .

Testează o singură acțiune - un singur API. Și stabilește clar așteptările testului prin simple afirmații de afirmare. De asemenea, testul nu implică codul terță parte prin împingere/batjocorire.

Începeți să scrieți teste de integrare

Când introduc codul nou în producție, dezvoltatorii (și toți ceilalți participanți la proiect) vor să fie siguri că noile funcții vor funcționa și că cele vechi nu se vor rupe.

Acest lucru este foarte greu de realizat fără testare și, dacă este făcut prost, poate duce la frustrare, oboseală a proiectului și, în cele din urmă, eșec al proiectului.

Testele de integrare, combinate cu testele unitare, reprezintă prima linie de apărare.

Folosirea doar a unuia dintre cele două este insuficientă și va lăsa mult spațiu pentru erori neacoperite. Folosirea mereu pe ambele va face noi angajamente solide și va oferi încredere și va inspira încredere tuturor participanților la proiect.