de Edo Rivai

Cum să batjocoresc cererile pentru testarea unității în nod

0*FDlur dky pPFMag
„Un vechi casetofon Philips și bandă așezată pe o podea de lemn din Italia” de Simone Acquaroli pe Unsplash

Să presupunem că ați decis să vă testați baza de cod și ați citit asta testele unitare și de integrare nu ar trebui să efectueze I / O. V-ați gândit că trebuie să eliminați cererile HTTP de ieșire pe care le face aplicația dvs., dar nu sunteți sigur de unde să începeți.

Am decis să-l întreb pe Kent C. Dodds pe twitter cum abordează jocul de batjocură HTTP:

Destul de corect, Kent! Cred că acest subiect merită o redactare mai elaborată.

TL; DR

Când trebuie să testați codul care trimite cereri HTTP, încercați următoarele.

ad-banner
  1. Împarte cererile HTTP din logica afacerii tale de procesare a răspunsului. Foarte des codul care gestionează chestiunile protocolului la nivel HTTP nu este foarte interesant și, fără îndoială, nu necesită testare. Utilizați instrumentul dvs. de batjocură la alegere pentru a-și moca înfășurarea API.
  2. Dacă într-adevăr trebuie să testați codul specific HTTP, iar răspunsul de la API-ul extern este relativ simplu, utilizați Nock și eliminați manual cererile.
  3. Dacă răspunsul pe care trebuie să-l testați este destul de complex, utilizați nock-record pentru a înregistra un răspuns o dată și pentru a utiliza înregistrarea respectivă pentru testele ulterioare.

Deoarece comunitatea de testare este obsedată de piramide, iată:

Cum sa batjocoresc cererile pentru testarea unitatii in nod
Piramida batjocoritoare HTTP. „API Wrappers + batjocură obișnuită” la bază. „Nocuri manuale” în mijloc. „Înregistrări Nock” în partea de sus.

introduce Nock

Aș spune că consensul general în terenul NodeJS este de a folosi nock , care funcționează prin corecția nativului Node http modul. Acest lucru funcționează foarte bine, deoarece chiar dacă nu utilizați http modul direct, majoritatea bibliotecilor utilizator-teren, cum ar fi axios, superagent , și node-fetch încă folosiți http sub capotă.

Scrierea și utilizarea unui Nock arata asa:

// Set up an interceptornock('http://www.example.com')  .post('/login', 'username=pgte&password=123456')  .reply(200, { id: '123ABC' });
// Run your code, which sends out a requestfetchUser('pgte', '123456');

În exemplul de mai sus, fetchUser va trimite o cerere POST la example.com/login . Nock va intercepta cererea și va răspunde imediat cu răspunsul dvs. predefinit, fără a atinge de fapt rețeaua. Minunat!

Nu este atât de simplu

Când am început cu Nock, am început cu nerăbdare să-l folosesc cu testele mele unitare. Cu toate acestea, aveam senzația rapidă că petrec mai mult timp scriind Nocks decât testând de fapt logica de afaceri. O soluție la acest lucru este să împărțiți codul solicitant de logica dvs. de afaceri. Să ne uităm la un cod.

async function getUser(id) {  const response = await fetch(`/api/users/${id}`);    // User does not exist  if (response.status === 404) return null;
  // Some other error occurred  if (response.status > 400) {    throw new Error(`Unable to fetch user #${id}`);  }    const { firstName, lastName } = await response.json();  return {    firstName,    lastName,    fullName: `${firstName} ${lastName}`  };}

Codul de mai sus trimite o solicitare către /api/users/<user id>, iar atunci când un utilizator este găsit, acesta primește un obiect conțineing a firstName and lastName. În cele din urmă, construiește un obiect, care are un supliment field fullName, care este calculat din numele și prenumele primite de la cerere.

O suită de testare pentru această funcție ar putea arăta astfel:

it('should properly decorate the fullName', async () => {  nock('http://localhost')    .get('/api/users/123')    .reply(200, { firstName: 'John', lastName: 'Doe });    const user = await getUser(123);  expect(user).toEqual({    firstName: 'John',    lastName: 'Doe,    fullName: 'John Doe'  });});
it('should return null if the user does not exist', async () => {  nock('http://localhost')    .get('/api/users/1337')    .reply(404);    const user = await getUser(1337);  expect(user).toBe(null);});
it('should return null when an error occurs', async () => {  nock('http://localhost')    .get('/api/users/42')    .reply(404);    const userPromise = getUser(42);  expect(userPromise).rejects.toThrow('Unable to fetch user #42');});

După cum puteți vedea, se întâmplă destul de multe în aceste teste. Să împărțim funcția în două părți:

  • cod care trimite și gestionează cererea HTTP
  • logica noastră de afaceri

Exemplul nostru este puțin inventat, deoarece singura logică de afaceri pe care o avem este să „calculăm” fullName . Dar vă puteți imagina cum o aplicație din lumea reală ar avea o logică de afaceri mai complexă.

// api.jsexport async function getUserFromApi(id) {  const response = await fetch(`/api/users/${id}`);    // User does not exist  if (response.status === 404) return null;
  // Some other error occurred  if (response.status > 400) {    throw new Error(`Unable to fetch user #${id}`);  }
  return response.json();}
// user.jsimport { getUserFromApi } from './api';
async function getUserWithFullName(id) {  const user = await getUserFromApi(id);  if (!user) return user;
  const { firstName, lastName } = user;  return {    firstName,    lastName,    fullName: `${firstName} ${lastName}`  };}

De dragul de a nu te plictisi până la moarte, îți voi arăta doar testele pentru logica noastră de afaceri. În loc să utilizați Nock pentru a elimina cererea HTTP, puteți utiliza acum biblioteca dvs. de batjocură la alegere pentru a elimina propriul nostru wrapper API. eu prefer Glumă, dar acest model nu este legat de nicio bibliotecă de batjocură specifică.

// The function we're testingimport { getUserWithFullName } from './user';
// Only imported for mockingimport { getUserFromApi } from './api';
jest.mock('./api');
it('should properly decorate the fullName', async () => {  getUserFromApi.mockResolvedValueOnce(    { firstName: 'John', lastName: 'Doe }  );    const user = await getUserWithFullName(123);  expect(user).toEqual({    firstName: 'John',    lastName: 'Doe,    fullName: 'John Doe'  });});
it('should return null if the user does not exist', async () => {  getUserFromApi.mockResolvedValueOnce(null);    const user = await getUserWithFullName(1337);  expect(user).toBe(null);});

După cum puteți vedea, testele noastre arată un pic mai curate. Toate cheltuielile HTTP sunt acum conținute în modulul API. Ceea ce am făcut efectiv este să minimizăm suprafața codului nostru care știe despre transportul HTTP. Și făcând acest lucru, reducem la minimum necesitatea utilizării Nock în testele noastre.

Dar logica HTTP este exact ceea ce vreau să testez!

Te aud. Uneori, conexiunea la un API extern este exact ceea ce doriți să testați.

Am arătat deja cum puteți utiliza Nock pentru a elimina o cerere HTTP de bază. Scrierea de Nocks explicite pentru astfel de perechi simple de solicitare / răspuns este foarte eficientă și aș recomanda să rămânem la ea cât mai mult posibil.

Cu toate acestea, uneori conținutul cererii sau al răspunsului poate deveni destul de complex. Scrierea cârligelor manuale pentru astfel de cazuri devine rapid plictisitoare și, de asemenea, fragilă!

Un exemplu foarte clar al unui astfel de caz ar fi testarea unui răzuitor. Principala responsabilitate a unui scraper este de a converti HTML brut în date utile. Cu toate acestea, atunci când vă testați răzuitorul, nu doriți să construiți manual o pagină HTML pentru a fi alimentată în Nock. Mai mult decât atât, site-ul pe care intenționați să-l răscoliți are deja codul HTML pe care doriți să îl procesați, așa că haideți să-l folosim! Gândiți-vă la Jest Snapshots, pentru batjocura HTTP.

Scraping subiecte din Medium

Să presupunem că vreau să știu toate subiectele disponibile pe Medium.

1611509467 100 Cum sa batjocoresc cererile pentru testarea unitatii in nod
Captură de ecran a paginii de pornire medium.com, afișând lista subiectelor disponibile

Vom folosi scrape-it pentru a solicita pagina principală Medium și a extrage textele din toate elementele care se potrivesc .ds-nav-item :

import scrapeIt from "scrape-it";
export function getTopics() {  return scrapeIt("https://medium.com", {    topics: {      listItem: ".ds-nav-item"    }  }).then(({ data }) => data.topics);}
// UsagegetTopics().then(console.log);// [ 'Home', 'Tech', 'Culture', 'Entrepreneurship', 'Self', 'Politics', 'Media', 'Design', 'Science', 'Work', 'Popular', 'More' ]

? Arata bine!

Acum, cum ne-am bate joc de cererea reală în testul nostru? O modalitate de a realiza acest lucru ar fi să accesați medium.com în browserul nostru, să vizualizați sursa și să copiați / lipiți manual un Nock. Acest lucru este plictisitor și predispus la erori. Dacă vrem cu adevărat întregul document HTML, am putea la fel de bine să lăsăm computerul să se ocupe de asta pentru noi.

Se pare Nock are un mecanism încorporat numit „Înregistrare”. Acest lucru vă permite să utilizați interceptorii Nock pentru a intercepta traficul HTTP real, apoi să stocați perechea cerere / răspuns într-un fișier și să utilizați acel înregistrare pentru cereri viitoare.

Personal, mi s-a părut foarte utilă funcționalitatea înregistrărilor Nock, dar ergonomia acestuia ar putea fi îmbunătățită. Deci, aici este a mea dop nerușinat pentru nock-record , o bibliotecă mai ergonomică pentru a beneficia de înregistrări:

0*jojs7J uR9k56M3C
Screencast de nock-record în acțiune. Afișând modul în care un test inițial trimite cereri HTTP efective, iar rulările ulterioare vor folosi înregistrările primei rulări pentru a preveni solicitările viitoare.

Să vedem cum ne-am putea testa racleta folosind nock-record :

import { setupRecorder } from 'nock-record';import { getTopics } from './index';
const record = setupRecorder();
describe('#getTopics', () => {  it('should get all topics', async () => {    // Start recording, specify fixture name    const { completeRecording } = await record('medium-topics');
    // Our actual function under test    const result = await getTopics();        // Complete the recording, allow for Nock to write fixtures    completeRecording();    expect(result).toEqual([      'Home',      'Tech',      'Culture',      'Entrepreneurship',      'Self',      'Politics',      'Media',      'Design',      'Science',      'Work',      'Popular',      'More'    ]);  });});

Prima dată când vom rula acest test, acesta va trimite solicitarea reală de a prelua codul HTML al paginii de pornire Medium:

✓ should get all topics (1163ms)

După prima alergare, nock-record a salvat înregistrarea într-un fișier la
__nock-fixtures__/medium-topics.json . Pentru a doua rundă, nock-record va încărca automat înregistrarea și va configura un Nock pentru dvs.

✓ should get all topics (116ms)

Dacă ați mai folosit instantanee Jest, acest flux de lucru vă va fi foarte familiar.

Acum am câștigat 3 lucruri folosind înregistrările:

  1. Deterministic: testul dvs. va rula întotdeauna pe același document HTML
  2. Rapid: testele ulterioare nu vor atinge rețeaua
  3. Ergonomic: nu este nevoie să jonglați manual cu dispozitivele de răspuns

Spune-mi ce crezi

Abordarea pe care am subliniat-o în acest articol a funcționat bine pentru mine. Mi-ar plăcea să aud despre experiența dvs. în comentarii sau pe twitter: @EdoRivai .

Același lucru este valabil și pentru nock-record; probleme și PR-uri sunt binevenite!