Testarea codului dvs. Ruby cu Garda, RSpec & Pry

Lucrarea mea recentă a fost făcută pe un proiect bazat pe nor, Ruby pentru BBC News, viitoarele alegeri din 2014. Aceasta necesită I / O rapidă, scalabilitate și trebuie să fie bine testată. Cerința "bine testată" este ceea ce vreau să mă concentrez în acest tutorial.

Introducere

Acest proiect utilizează câteva servicii Amazon diferite, cum ar fi:

  • SQS (serviciu simplu de coadă)
  • DynamoDB (magazin cheie / valoare)
  • S3 (serviciu de stocare simplă)

Trebuie să fim capabili să scriem teste care sunt rapide și să ne dea feedback instantaneu asupra problemelor legate de codul nostru.

Deși nu vom folosi serviciile Amazon în acest tutorial, le menționez pentru că pentru a avea teste care sunt rapide, ne cere să falsăm aceste obiecte externe (de exemplu, nu ar trebui să avem nevoie de o conexiune de rețea pentru a ne rula teste, deoarece această dependență poate duce la teste de funcționare lentă).

Alaturi de tehnicianul Robert Kenny (care este foarte bine pregatit in dezvoltarea aplicatiilor Ruby TDD (based on test-based), am folosit diferite instrumente care au facut acest proces si programul nostru de lucru mult mai usor.

Vreau să vă împărtășesc câteva informații despre aceste instrumente.

Instrumentele pe care le voi acoperi sunt:

  • RSPE (cadrul de testare)
  • Garda (alergător)
  • Pry (REPL și depanare)

Ce trebuie să știu în avans?

Voi face o presupunere că sunteți familiarizat cu codul Ruby și ecosistemul Ruby. De exemplu, nu ar trebui să vă explic cum sunt "pietrele prețioase" sau cum funcționează anumite sintaxe / concepte Ruby.

Dacă nu sunteți sigur, atunci înainte de a vă deplasa, vă recomandăm să citiți una dintre celelalte postări ale mele pe Ruby pentru a vă ridica la viteză.

guard

S-ar putea să nu fiți familiarizați cu Garda, dar în esență este un instrument de linie de comandă care utilizează Ruby pentru a gestiona diferite evenimente.

De exemplu, Garda vă poate notifica ori de câte ori anumite fișiere au fost editate și puteți efectua anumite acțiuni în funcție de tipul de fișier sau eveniment care a fost concediat.

Acest lucru este cunoscut ca un "alergător de sarcini", probabil că ați auzit fraza înainte, deoarece în prezent sunt foarte folosiți în lumea frontului / clientului (Grunt și Gulp sunt două exemple populare).

Motivul pentru care vom folosi Garda este pentru că ajută la întărirea buclă de feedback (atunci când faci TDD). Aceasta ne permite să editați fișierele noastre de testare, să examinăm un test de eroare, să actualizăm și să salvăm codul nostru și să vedem imediat dacă trece sau nu (în funcție de ceea ce am scris).

Ați putea folosi ceva asemănător cu Grunt sau Gulp, dar preferăm să folosiți acele tipuri de alergători pentru a manipula lucruri de la front-end / client. Pentru codul back-end / server-side, folosim Rake și Guard.

RSpec

RSpec, dacă nu știați deja, este un instrument de testare pentru limbajul de programare Ruby.

Puteți executa testele (utilizând RSpec) prin linia de comandă și voi demonstra cum puteți face acest proces mai ușor prin utilizarea programului de construire a lui Ruby, Rake.

Desface

În sfârșit, vom folosi o altă bijuterie Ruby numită Pry, care este un instrument extrem de puternic de depanare a erorilor Ruby, care se injectează în aplicația dvs., în timp ce rulează, pentru a vă permite să inspectați codul și să aflați de ce ceva nu funcționează.

TDD (dezvoltare bazată pe teste)

Deși nu este necesar pentru a demonstra utilizarea RSpec și Guard, merită menționat faptul că susțin pe deplin utilizarea TDD ca mijloc de a asigura că fiecare linie de cod pe care o scrieți are un scop și a fost proiectată într-o manieră testabilă și fiabilă.

Voi detalia cum am face TDD cu o aplicație simplă, deci cel puțin veți obține o simțire a modului în care funcționează procesul.

Crearea unui exemplu de proiect

Am creat un exemplu de bază pentru GitHub, pentru a vă salva de la faptul că trebuie să tastați totul singur. Nu ezitați să descărcați codul. 

Să începem acum și să examinăm acest proiect, pas cu pas.

Fișiere primare

Există trei fișiere primare necesare pentru ca aplicația noastră de exemplu să funcționeze, acestea sunt:

  1. Gemfile
  2. Guardfile
  3. Rakefile

Vom trece în scurt timp conținutul fiecărui fișier, dar primul lucru pe care trebuie să-l facem este să introducem structura noastră de directoare.

Structura directorului

Pentru proiectul nostru de exemplu, vom avea nevoie de două dosare create:

  • lib (acest lucru va conține codul nostru de aplicare)
  • spec (aceasta va conține codul nostru de testare)

Aceasta nu este o cerință pentru aplicația dvs., puteți modifica cu ușurință codul în cadrul celorlalte fișiere pentru a lucra cu structura potrivită.

Instalare

Deschideți terminalul și executați următoarea comandă:

gem install bundler

Bundler este un instrument care facilitează instalarea altor pietre prețioase.

Odată ce ați executat comanda respectivă, creați cele trei fișiere de mai sus (Gemfile, Guardfile și Rakefile).

Gemfile

Gemfile este responsabil pentru definirea unei liste de dependențe pentru aplicația noastră.

Iată cum arată:

sursă "https://rubygems.org" gem 'rspec "grup: dezvoltare do gem' paza 'gem' guard-rspec 'gem' pry 'end 

Odată ce acest fișier este salvat, executați comanda instalare pachet.

Acest lucru va instala toate pietrele noastre pentru noi (inclusiv cele pietre precizate în cadrul dezvoltare grup).

Scopul dezvoltare (care este o caracteristică specifică a pachetului) este atunci când implementați aplicația dvs., puteți spune mediului dvs. de producție să instaleze numai pietrele care sunt necesare pentru ca aplicația să funcționeze corect.

Deci, de exemplu, toate pietre în interiorul dezvoltare grup, nu sunt necesare pentru buna funcționare a aplicației. Acestea sunt folosite numai pentru a ne ajuta în timp ce dezvoltăm și testează codul nostru.

Pentru a instala pietre potrivite pe serverul de producție, ar trebui să executați ceva de genul:

pachet de instalare - fără dezvoltare

Rakefile

Rakefile ne va permite să executăm testele RSpec din linia de comandă.

Iată cum arată:

cereți "rspec / core / rake_task" RSpec :: Core :: RakeTask.new nu | task.rspec_opts = ['--color', '--format', 'doc'] sfârșit 

Notă: nu aveți nevoie de gardă pentru a putea executa testele RSpec. Folosim Garda pentru a face mai ușor TDD-ul.

Când instalați RSpec, vă oferă acces la o sarcină construită în Rake și asta este ceea ce folosim aici.

Creăm o nouă instanță de RakeTask care implicit creează o sarcină numită spec care va căuta un dosar numit spec și va rula toate fișierele de testare din acel folder, utilizând opțiunile de configurare pe care le-am definit.

În acest caz, dorim ca outputul shell-ului nostru să aibă culori și dorim să formăm ieșirea la medic stil (puteți schimba formatul care urmează să fie cuibărit ca exemplu).

Puteți configura sarcina Rake să funcționeze în orice fel doriți și să căutați în directoare diferite, dacă asta aveți. Dar setările implicite funcționează excelent pentru aplicația noastră și așa vom folosi.

Acum, dacă vreau să rulez testele în depozitul GitHub al exemplului, atunci trebuie să-mi deschid terminalul și să execut comanda:

rake spec

Acest lucru ne oferă următoarea ieșire:

rake spec / bin / ruby ​​-S rspec ./spec/example_spec.rb --color --format doc RSpecGreeter RSpecGreeter # greet () Terminat în 0.0006 secunde 1 exemplu, 0 eșecuri 

După cum puteți vedea, există eșecuri zero. Asta pentru că, deși nu avem codul aplicației scrise, nu avem încă nici un cod de test scris.

Guardfile

Conținutul acestui fișier spune Garda ce să facă atunci când rulați pază comanda:

pază "rspec" nu # watch / lib / fișiere ceas (% r ^ lib /(.+). "spec / # m] [1] _ spec.rb" sfârșitul # ceas / spec / fișiere ceas (% r ^ spec /(.+). "spec / # m [1]. rb" sfârșitul final 

Veți observa în interiorul nostru Gemfile am specificat bijuteria: guard-rspec. Avem nevoie de acea bijuterie pentru a permite Gardienilor să înțeleagă cum să se ocupe de modificările aduse fișierelor legate de RSpec.

Dacă ne uităm din nou la conținut, putem vedea că dacă am fugit paza rspec atunci Guard urma să urmărească fișierele specificate și să execute comenzile specificate odată ce s-au produs modificările acestor fișiere.

Notă: pentru că avem doar o sarcină de pază, rspec, atunci aceasta se execută în mod implicit dacă am executat comanda pază.

Poți să vezi că Garda ne oferă ceas funcția pe care noi o transmitem printr-o Expresie regulată pentru a ne permite să definim ce fișiere suntem interesați să urmărim Garda.

În acest caz, îi spunem Gardei să vadă toate fișierele din cadrul nostru lib și spec foldere și în cazul în care orice modificări apar la oricare dintre aceste fișiere, apoi pentru a executa fișierele de testare în cadrul nostru spec dosar pentru a vă asigura că nu s-au făcut modificări pe care le-am încălcat testele noastre (și, ulterior, nu ne-am rupt codul).

Dacă aveți toate fișierele descărcate de la GitHub repo, atunci puteți încerca comanda pentru tine.

Alerga pază și apoi salvați unul din fișiere pentru a vedea că rulează testele.

Cod de test

Înainte de a începe să examinăm un cod de testare și de aplicare, permiteți-mi să explic ce va face aplicația noastră. Aplicația noastră este o singură clasă care va returna un mesaj de salut tuturor celor care execută codul.

Cerințele noastre sunt simplificate intenționat, deoarece vor face mai ușor înțelegerea procesului pe care urmează să-l realizăm.

Să luăm acum o privire la un exemplu de specificație (de exemplu, fișierul nostru de testare) care va descrie cerințele noastre. După aceasta, vom începe să trecem prin codul definit în caietul de sarcini și vom vedea cum putem folosi TDD pentru a ne ajuta în scrierea cererii noastre.

Primul nostru test

Vom crea un fișier intitulat example_spec.rb. Scopul acestui fișier este de a deveni fișierul cu specificațiile noastre (cu alte cuvinte, acesta va fi codul nostru de testare și va reprezenta funcționalitatea așteptată).

Motivul pentru care scriem codul nostru de testare înainte de a scrie codul nostru actual de aplicație este că, în cele din urmă, înseamnă că orice cod de aplicație pe care îl producem va exista pentru că a fost folosit de fapt.

Acesta este un punct important pe care îl fac și permiteți-mi să iau o clipă pentru ao clarifica mai detaliat.

Scrierea codului de testare înainte de aplicarea codului

În mod tipic, dacă mai întâi scrieți codul aplicației (deci nu faceți TDD), atunci veți găsi că scrieți un cod care, la un moment dat în viitor, este depășit și ar putea fi depășit. Prin procesul de refactorizare sau schimbarea cerințelor, puteți constata că anumite funcții nu vor fi niciodată chemate.

Acesta este motivul pentru care TDD este considerată o practică mai bună și o metodă de dezvoltare preferată de utilizat, deoarece fiecare linie de cod pe care o produceți va fi produsă dintr-un motiv: pentru a obține o specificație defectuoasă (cerința dvs. reală de afaceri) să treacă. Este un lucru foarte puternic pe care să-l aveți în minte.

Iată codul nostru de testare:

cer "spec_helper" descrie 'RSpecGreeter' fa -l 'RSpecGreeter # greet ()' do greeter = RSpecGreeter.new # dat salut = greeter.greet # 

Este posibil să observați comentariile codului la sfârșitul fiecărui rând:

  • Dat
  • Cand
  • Atunci

Acestea sunt o formă de terminologie BDD (Development-Driven Development). Le-am inclus pentru cititorii care sunt mai familiarizați cu BDD (Behavior-Driven Development) și care au fost interesați de modul în care pot echivala aceste afirmații cu TDD.

Primul lucru pe care îl facem în acest fișier este încărcarea spec_helper.rb (care se găsește în același director ca fișierul nostru de spec.). Vom reveni și vom privi conținutul fișierului într-o clipă.

Apoi avem două blocuri de coduri specifice pentru RSpec:

  • descrie "x"
  • ce faci

Primul descrie bloc ar trebui să descrie în mod adecvat clasa / modulul pe care lucrăm și să oferim teste pentru. Ai putea avea foarte multe descrie blochează într-un singur fișier cu specificații.

Există multe teorii diferite cu privire la modul de utilizare descrie și aceasta blocuri de descriere. Eu personal prefer simplitatea si astfel voi folosi identificatorii pentru clasa / module / metode pe care le vom testa. Dar găsiți adesea unii oameni care preferă să folosească propoziții complete pentru descrierile lor. Nici nu este bine sau nu, doar preferința personală.

aceasta bloc este diferit și ar trebui să fie întotdeauna plasat în interior descrie bloc. Ar trebui să explice Cum dorim ca aplicația noastră să funcționeze.

Din nou, ați putea folosi o propoziție normală pentru a descrie cerințele, dar am constatat că uneori acest lucru poate determina descrierile să fie prea explicite, când ar trebui să fie într-adevăr mai mult implicit. Fiind mai puțin explicită reduce șansele de schimbare a funcționalității dvs., determinând descrierea dvs. să devină depășită (trebuie să actualizați descrierea de fiecare dată când apar modificări minore ale funcționalității, este mai mult o sarcină decât un ajutor). Folosind identificatorul metodei pe care o testăm (de exemplu, numele metodei pe care o executăm) putem evita acea problemă.

Conținutul mesajului aceasta bloc este codul pe care îl vom testa.

În exemplul de mai sus, creăm o nouă instanță a clasei RSpecGreeter (care nu există încă). Trimitem mesajul întâmpina (care, de asemenea, nu există încă) pentru obiectul instanțiat creat (Notă: aceste două linii sunt codul Ruby standard în acest moment).

În cele din urmă, spunem cadrului de testare că ne așteptăm la rezultatul chemării întâmpina metoda de a fi textul "Bună ziua RSpec!", utilizând sintaxa RSpec: echiv (ceva).

Observați cum permite sintaxa să fie ușor de citit (chiar de către o persoană non-tehnică). Acestea sunt cunoscute sub numele de afirmații.

Există o mulțime de afirmații RSpec diferite și nu vom intra în detalii, dar nu ezitați să revizuiți documentația pentru a vedea toate caracteristicile pe care RSpec le oferă.

Crearea unui ajutor pentru testul nostru

Există o anumită cantitate de boilerplate necesară testărilor noastre. În acest proiect, avem doar un fișier de specificații, dar într-un proiect real este posibil să aveți zeci (în funcție de dimensiunea aplicației).

Pentru a ne ajuta să reducem codul de boilerplate, îl vom plasa în interiorul unui fișier special de ajutor pe care îl vom încărca din fișierele cu specificații. Acest fișier va fi intitulat spec_helper.rb.

Acest fișier va face câteva lucruri:

  • spuneți Ruby unde este localizat codul nostru principal de aplicație
  • încărcați codul aplicației (pentru ca testele să se împotrivească)
  • încărcați desface gem (ne ajută să depanem codul nostru, dacă este necesar).

Iată codul:

$ << File.join(File.dirname(FILE), '… ', 'lib') require 'pry' require 'example' 

Notă: prima linie poate părea un pic criptică, așa că permiteți-mi să explic cum funcționează. Aici spunem că vrem să adăugăm / Lib / dosarul lui Ruby $ LOAD_PATH variabilă sistem. Ori de câte ori Ruby evaluează cere "some_file" are o listă de directoare pe care va încerca să le găsească. În acest caz, ne asigurăm că dacă avem codul cereți "exemplu" că Ruby va fi capabil să-l localizeze pentru că ne va verifica / Lib / director și acolo, va găsi fișierul specificat. Acesta este un truc inteligent pe care îl veți vedea folosit într-o mulțime de pietre Ruby, dar poate fi destul de confuz dacă nu ați mai văzut-o înainte.

Codul aplicației

Codul nostru de aplicare va fi în interiorul unui fișier intitulat example.rb.

Înainte de a începe să scrieți orice cod de aplicație, amintiți-vă că facem acest proiect TDD. Așa că vom lăsa ca testele din fișierul cu caietul de sarcini să ne ghideze pe ce să facem mai întâi.

Să începem presupunând că o folosiți pază pentru a rula testele dvs. (de fiecare dată când facem o schimbare example.rb, Garda va observa schimbarea și va continua să ruleze example_spec.rb pentru a vă asigura că testele noastre trec).

Pentru ca noi să facem TDD în mod corespunzător, nostru example.rb fișierul va fi gol și dacă deschidem fișierul și îl salvăm în starea sa actuală, Guard va rula și vom descoperi (în mod surprinzător) că testul nostru va eșua:

Eșecurile: 1) RSpecGreeter RSpecGreeter # greet () Eroare / eroare: greeter = RSpecGreeter.new # Datând numeleError: constanta necondiționată RSpecGreeter # ./spec/example_spec.rb:5:inbloc (2 nivele) în 'Finalizat în 0.00059 secunde 1 exemplu, 1 eșec Exemple greșite: rspec ./spec/example_spec.rb:4 # RSpecGreeter RSpecGreeter # greet ()

Acum, înainte de a merge mai departe, permiteți-mi să clarific din nou că TDD se bazează pe premisa că fiecare linie de cod are un motiv să existe, deci nu face începeți curse înainte și scrieți mai mult cod decât aveți nevoie. Scrieți doar cantitatea minimă de cod necesară pentru ca testul să treacă. Chiar dacă codul este urât sau nu îndeplinește funcționalitatea completă.

Punctul lui TDD este acela de a fi strâns bucla de feedback, cunoscută și ca "roșu, verde, refactor"). Ce înseamnă acest lucru în practică este:

  • scrieți un test de eșec
  • scrieți cel mai mic număr de cod pentru al trece
  • refactor codul

Veți vedea într-o clipă că, deoarece cerințele noastre sunt atât de simple, nu este nevoie ca noi să refacem. Dar într-un proiect real cu cerințe mult mai complexe, probabil că va trebui să faceți al treilea pas și să refaceți codul pe care l-ați introdus, pentru ca testul să treacă.


Revenind la testul nostru de eșec, așa cum puteți vedea în eroare, nu există RSpecGreeter clasă definită. Să rezolvăm acest lucru și să adăugăm următorul cod și să salvăm fișierul astfel încât testele să fie executate:

clasa RSpecGreeter # cod va merge în cele din urmă aici sfârșitul 

Aceasta va duce la următoarea eroare:

Erori: 1) RSpecGreeter RSpecGreeter # greet () Eroare / Eroare: greeter = greeter.greet # Când NoMethodError: undefined methodgreet 'pentru # # .spec/example_spec.rb:6:in block (2 nivele) 0.00036 secunde 1 exemplu, 1 eșec 

Acum vedem că această eroare ne spune metoda întâmpina nu există, deci să o adăugăm și apoi să salvăm din nou fișierul nostru pentru a rula testele noastre:

clasa RSpecGreeter def salut # cod va veni în cele din urmă aici sfârșitul sfârșitul 

OK, suntem aproape acolo. Eroarea pe care o avem acum este:

Erori: 1) RSpecGreeter RSpecGreeter # greet () Eșec / greșeală: greeter = greeting.should eq ('Hello RSpec!') # Apoi așteptați: "Hello RSpec! a primit: nil (comparat folosind ==) # ./spec/example_spec.rb:7:in 'bloc (2 niveluri) în' Finalizat în 0.00067 secunde 1 exemplu, 1 eșec 

RSpec ne spune că se aștepta să vadă Bună ziua RSpec! dar în schimb a ajuns zero (pentru că am definit întâmpina dar nu a definit nimic în interiorul metodei și astfel se întoarce zero).

Vom adăuga restul de cod care ne-ar face testul să treacă:

clasa RSpecGreeter def salut "Hello RSpec!" sfârșitul final 

Acolo o avem, un test de trecere:

Terminat în 0.00061 secunde 1 exemplu, 0 eșecuri 

Am terminat aici. Testul nostru este scris și codul trece.

Concluzie

Până acum, am aplicat un proces de dezvoltare bazat pe teste pentru a construi aplicația noastră, alături de utilizarea cadrului popular de testare RSpec.

O vom lăsa aici deocamdată. Întoarce-te și adaugă-ne pentru partea a doua, unde vom analiza mai multe caracteristici RSpec specifice, precum și folosind gemul Pry pentru a vă ajuta să depanați și să scrieți codul.

Cod