Mijn recente werk was op een cloud-gebaseerd Ruby-project voor de BBC News, komende verkiezingen in 2014. Het vereist snelle I / O, schaalbaarheid en moet goed worden getest. De eis "goed getest" is waar ik me in deze tutorial op wil concentreren.
Dit project maakt gebruik van een paar verschillende Amazon-services, zoals:
We moeten tests kunnen schrijven die snel zijn en ons direct feedback geven over problemen met onze code.
Hoewel we in deze zelfstudie geen gebruik zullen maken van Amazon-services, noem ik ze omdat het voor ons om snelle testen te zijn, vereist dat we deze externe objecten nep maken (we zouden bijvoorbeeld geen netwerkverbinding moeten hebben om onze tests, omdat die afhankelijkheid kan resulteren in trage tests).
Samen met tech lead Robert Kenny (die zeer goed thuis is in het schrijven van TDD (test-driven development) gebaseerde Ruby-applicaties) hebben we verschillende tools gebruikt die dit proces en ons programmeren een stuk eenvoudiger hebben gemaakt.
Ik wil wat informatie over deze hulpmiddelen met u delen.
De tools die ik ga behandelen zijn:
Ik ga ervan uit dat je bekend bent met de Ruby-code en het Ruby-ecosysteem. Ik hoef u bijvoorbeeld niet uit te leggen wat 'edelstenen' zijn of hoe bepaalde Ruby-syntaxis / concepten werken.
Als je het niet zeker weet, raad ik aan om een van mijn andere posts op Ruby te lezen voordat je verder gaat..
U bent misschien niet bekend met Guard, maar in wezen is het een opdrachtregelprogramma dat Ruby gebruikt om verschillende gebeurtenissen te verwerken.
Guard kan u bijvoorbeeld op de hoogte houden wanneer bepaalde bestanden zijn bewerkt en u kunt enige actie uitvoeren op basis van het type bestand of de gebeurtenis die is geactiveerd.
Dit staat bekend als een 'taak runner', je hebt misschien de zin eerder gehoord, omdat ze op dit moment veel gebruik maken in de front-end / client-side wereld (Grunt en Gulp zijn twee populaire voorbeelden).
De reden dat we Guard gebruiken is omdat het helpt om de feedbacklus (bij het doen van TDD) een stuk strakker te maken. Het stelt ons in staat om onze testbestanden te bewerken, een falende test te zien, onze code bij te werken en op te slaan en onmiddellijk te zien of deze slaagt of faalt (afhankelijk van wat we schreven).
In plaats daarvan zou je iets als Grunt of Gulp kunnen gebruiken, maar we geven er de voorkeur aan om die soorten taaklopers te gebruiken voor het afhandelen van front-end / client-side dingen. Voor back-end / server-side code gebruiken we Rake en Guard.
RSpec is, als u dat nog niet wist, een testtool voor de programmeertaal van Ruby.
U voert uw tests (met behulp van RSpec) uit via de opdrachtregel en ik zal laten zien hoe u dit proces gemakkelijker kunt maken met behulp van het build-programma van Ruby, Rake.
Ten slotte gebruiken we nog een Ruby-juweel met de naam Pry, een uiterst krachtige Ruby-foutopsporingshulpprogramma die zichzelf in uw toepassing injecteert, terwijl deze wordt uitgevoerd, zodat u uw code kunt inspecteren en kunt achterhalen waarom iets niet werkt.
Hoewel het niet nodig is om het gebruik van RSpec en Guard te demonstreren, is het vermeldenswaard dat ik het gebruik van TDD volledig onderschrijf als een middel om ervoor te zorgen dat elke regel met code die je schrijft een doel heeft en op een toetsbare en betrouwbare manier is ontworpen.
Ik zal in detail beschrijven hoe we TDD met een eenvoudige applicatie zouden doen, zodat je tenminste een idee krijgt hoe het proces werkt.
Ik heb een eenvoudig voorbeeld gemaakt op GitHub om te voorkomen dat je alles zelf moet typen. Je bent vrij om de code te downloaden.
Laten we dit project nu stap voor stap bespreken.
Er zijn drie primaire bestanden die nodig zijn om onze voorbeeldtoepassing te laten werken, dit zijn:
Gemfile
Guardfile
Rakefile
We zullen de inhoud van elk bestand binnenkort bespreken, maar het eerste dat we moeten doen is onze directorystructuur op orde krijgen.
Voor ons voorbeeldproject hebben we twee mappen nodig gemaakt:
lib
(dit bevat onze applicatiecode)spec
(dit houdt onze testcode vast)Dit is geen vereiste voor uw toepassing, u kunt de code eenvoudig aanpassen binnen onze andere bestanden om te werken met elke structuur die bij u past.
Open uw terminal en voer de volgende opdracht uit:
gem installeer bundler
Bundler is een tool die het installeren van andere edelstenen eenvoudiger maakt.
Nadat u die opdracht hebt uitgevoerd, maakt u de bovenstaande drie bestanden (Gemfile
, Guardfile
en Rakefile
).
De Gemfile
is verantwoordelijk voor het definiëren van een lijst met afhankelijkheden voor onze applicatie.
Hier is hoe het eruit ziet:
bron "https://rubygems.org" gem 'rspec' groep: ontwikkeling doe edel 'bewaker' edelsteen 'bewaker-rspec' edelsteen 'wrikken' einde
Zodra dit bestand is opgeslagen, voert u de opdracht uit bundel installeren
.
Dit installeert al onze edelstenen voor ons (inclusief die edelstenen die gespecificeerd zijn in de ontwikkeling
groep).
Het doel van de ontwikkeling
groep (wat een bundler-specifieke functie is), is dus wanneer u uw toepassing implementeert, kunt u uw productieomgeving vertellen om alleen de edelstenen te installeren die nodig zijn voor uw applicatie om correct te functioneren.
Dus bijvoorbeeld alle edelstenen in de ontwikkeling
groep, zijn niet vereist voor de juiste werking van de toepassing. Ze worden alleen gebruikt om ons te helpen terwijl we onze code ontwikkelen en testen.
Om de juiste edelstenen op uw productieserver te installeren, moet u iets als:
bundel installeren - zonder ontwikkeling
De Rakefile
stelt ons in staat om onze RSpec-tests vanaf de opdrachtregel uit te voeren.
Hier is hoe het eruit ziet:
vereisen 'rspec / core / rake_task' RSpec :: Core :: RakeTask.new do | task | task.rspec_opts = einde van ['--color', '--format', 'doc']
Notitie: u hebt Guard niet nodig om uw RSpec-tests te kunnen uitvoeren. We gebruiken Guard om het gemakkelijker te maken om TDD te doen.
Wanneer u RSpec installeert, geeft dit u toegang tot een ingebouwde Rake-taak en dat is wat we hier gebruiken.
We maken een nieuw exemplaar van RakeTask
die standaard een taak creëert genaamd spec
die naar een map zoekt die wordt genoemd spec
en zal alle testbestanden in die map uitvoeren, met behulp van de configuratie-opties die we hebben gedefinieerd.
In dit geval willen we dat onze shell-uitvoer kleur heeft en we willen de uitvoer naar de indeling dokter
stijl (je zou het formaat kunnen veranderen genesteld
als voorbeeld).
U kunt de Rake-taak zo configureren dat deze op elke gewenste manier werkt en in verschillende mappen kijkt, als dat is wat u heeft. Maar de standaardinstellingen werken goed voor onze applicatie en dat is wat we zullen gebruiken.
Als ik nu de tests in mijn voorbeeld GitHub-repository wil uitvoeren, moet ik mijn terminal openen en de opdracht uitvoeren:
rake spec
Dit geeft ons de volgende output:
hake spec / bin / ruby -S rspec ./spec/example_spec.rb --color --format doc RSpecGreeter RSpecGreeter # greet () Klaar in 0.0006 seconden 1 voorbeeld, 0 mislukkingen
Zoals je kunt zien zijn er nul mislukkingen. Dat komt omdat hoewel we geen applicatiecode hebben geschreven, we ook nog geen testcode hebben geschreven.
De inhoud van dit bestand vertelt Guard wat te doen als we de bewaker
commando:
bewaken 'rspec' do # watch / lib / files watch (% r ^ lib /(.+). rb $) do | m | "spec / # m [1] _ spec.rb" end # watch / spec / files watch (% r ^ spec /(.+). rb $) do | m | "spec / # m [1]. rb" einde
Je zult het gemerkt hebben in onze Gemfile
we hebben de edelsteen gespecificeerd: guard-rspec
. We hebben die edelsteen nodig om Guard te laten begrijpen hoe om te gaan met veranderingen in RSpec-gerelateerde bestanden.
Als we opnieuw naar de inhoud kijken, kunnen we zien dat als we liepen bewaak rspec
dan zou Guard de gespecificeerde bestanden bekijken en de gespecificeerde commando's uitvoeren zodra er wijzigingen in die bestanden waren opgetreden.
Notitie: omdat we maar één bewakingstaak hebben, rspec
, dan wordt dat standaard uitgevoerd als we de opdracht uitvoeren bewaker
.
Je kunt zien dat Guard ons voorziet van een kijk maar
functie waarmee we een reguliere expressie passeren om ons in staat te stellen te definiëren in welke bestanden we geïnteresseerd zijn in Guard Watching.
In dit geval vertellen we Guard om alle bestanden in onze map te bekijken lib
en spec
mappen en als er wijzigingen optreden in een van die bestanden om vervolgens de testbestanden uit te voeren binnen onze spec
map om ervoor te zorgen dat geen wijzigingen die we hebben aangebracht onze tests hebben verbroken (en hebben vervolgens onze code niet overtreden).
Als je alle bestanden van de GitHub-repo hebt gedownload, kun je de opdracht zelf uitproberen.
Rennen bewaker
en sla vervolgens een van de bestanden op om te zien dat de tests worden uitgevoerd.
Voordat we beginnen met het bekijken van een aantal test- en toepassingscode, wil ik eerst uitleggen wat onze applicatie gaat doen. Onze applicatie is een enkele klasse die een begroeting terugstuurt naar degene die de code gebruikt.
Onze vereisten zijn opzettelijk vereenvoudigd, omdat het proces dat we nu gaan ondernemen, gemakkelijker te begrijpen is.
Laten we nu eens kijken naar een voorbeeldspecificatie (bijvoorbeeld ons testbestand) waarin onze vereisten worden beschreven. Daarna gaan we door de code die in de specificatie is gedefinieerd en bekijken we hoe we TDD kunnen gebruiken om ons te helpen bij het schrijven van onze applicatie.
We gaan een bestand maken met de naam example_spec.rb
. Het doel van dit bestand is om ons specificatiebestand te worden (met andere woorden, dit wordt onze testcode en vertegenwoordigt de verwachte functionaliteit).
De reden dat we onze testcode schrijven voordat we onze eigenlijke applicatiecode schrijven, is omdat het uiteindelijk betekent dat elke applicatiecode die we produceren, bestaat omdat deze feitelijk is gebruikt.
Dat is een belangrijk punt dat ik aan het maken ben en daarom wil ik een moment nemen om het in meer detail te verduidelijken.
Typisch, als u eerst uw toepassingscode schrijft (dus u doet geen TDD), dan zult u merken dat u code schrijft die op enig moment in de toekomst overdreven geëmplementeerd en mogelijk achterhaald is. Door het proces van het refactoren of wijzigen van vereisten, zult u merken dat sommige functies nooit zullen worden genoemd.
Dit is de reden waarom TDD wordt beschouwd als de betere methode en de geprefereerde ontwikkelmethode om te gebruiken, omdat elke regel met code die u produceert, om een reden is geproduceerd: om een falende specificatie (uw werkelijke bedrijfsbehoefte) te behalen. Dat is een zeer krachtig iets om in gedachten te houden.
Hier is onze testcode:
vereisen 'spec_helper' beschrijven 'RSpecGreeter' doe het 'RSpecGreeter # greet ()' do greeter = RSpecGreeter.new # Given greeting = greeter.greet # When greeting.should eq ('Hello RSpec!') # Then end end
Mogelijk ziet u de opmerkingen aan het einde van elke regel:
Gegeven
Wanneer
Dan
Dit zijn een vorm van BDD (Behavior-Driven Development) terminologie. Ik nam ze op voor lezers die meer vertrouwd zijn met BDD (Behavior-Driven Development) en die geïnteresseerd waren in hoe ze deze uitspraken kunnen gelijkstellen met TDD.
Het eerste dat we in dit bestand doen, is laden spec_helper.rb
(die is te vinden in dezelfde map als ons spec-bestand). We komen terug en bekijken de inhoud van dat bestand in een oogwenk.
Vervolgens hebben we twee codeblokken die specifiek zijn voor RSpec:
beschrijf 'x' doen
het is 'doe'
De eerste beschrijven
blok moet de specifieke klasse / module waarop we werken adequaat beschrijven en testen aanbieden. Je zou heel goed meerdere kunnen hebben beschrijven
blokken binnen één specificatiebestand.
Er zijn veel verschillende theorieën over hoe te gebruiken beschrijven
en het
beschrijving blokken. Ik geef persoonlijk de voorkeur aan eenvoud en daarom zal ik de ID's gebruiken voor de Class / Modules / Methods die we gaan testen. Maar je vindt vaak mensen die liever volledige zinnen gebruiken voor hun beschrijvingen. Evenmin is goed of fout, alleen persoonlijke voorkeur.
De het
blok is anders en moet altijd in een beschrijven
blok. Het zou moeten uitleggen hoe we willen dat onze applicatie werkt.
Nogmaals, je zou een normale zin kunnen gebruiken om de vereisten te beschrijven, maar ik heb gemerkt dat dit soms kan veroorzaken dat de beschrijvingen te expliciet zijn, wanneer ze echt meer zouden moeten zijn stilzwijgend. Minder expliciet zijn, verkleint de kans op wijzigingen in uw functionaliteit, waardoor uw beschrijving verouderd raakt (elke keer dat u kleine wijzigingen in de functionaliteit aanbrengt, moet u uw beschrijving bijwerken is meer een last dan een hulp). Door het ID van de methode te gebruiken die we testen (bijvoorbeeld de naam van de methode die we uitvoeren), kunnen we dat probleem voorkomen.
De inhoud van de het
blok is de code die we gaan testen.
In het bovenstaande voorbeeld maken we een nieuw exemplaar van de klasse RSpecGreeter
(die nog niet bestaat). We sturen het bericht begroeten
(dat ook nog niet bestaat) naar het gemaakte gemaakte object (Notitie: deze twee regels zijn op dit moment standaard Ruby-code).
Ten slotte vertellen we aan het testraamwerk dat we de uitkomst van het oproepen van de begroeten
methode om de tekst te zijn "Hallo RSpec!
", door de RSpec-syntaxis te gebruiken: eq (iets)
.
Merk op hoe de syntaxis het toestaat om gemakkelijk te lezen (zelfs door een niet-technische persoon). Deze staan bekend als beweringen.
Er zijn veel verschillende RSpec-beweringen en we zullen niet ingaan op de details, maar voel je vrij om de documentatie te bekijken om alle functies te zien die RSpec biedt.
Er is een bepaalde hoeveelheid boilerplate vereist om onze tests uit te voeren. In dit project hebben we slechts één specificatiebestand, maar in een echt project zult u waarschijnlijk tientallen hebben (afhankelijk van de grootte van uw applicatie).
Om ons te helpen de boilerplate-code te verlagen, plaatsen we deze in een speciaal helperbestand dat we uit onze specificatiebestanden zullen laden. Dit bestand heeft de titel spec_helper.rb
.
Dit bestand zal een aantal dingen doen:
wrikken
edelsteen (helpt ons om onze code te debuggen, als dat nodig is).Hier is de code:
$ << File.join(File.dirname(FILE), '… ', 'lib') require 'pry' require 'example'
Notitie: de eerste regel ziet er misschien wat cryptisch uit, dus laat me uitleggen hoe het werkt. Hier zeggen we dat we het willen toevoegen / Lib /
map naar Ruby's $ LOAD_PATH
systeemvariabele. Wanneer Ruby evalueert vereisen 'some_file'
het heeft een lijst van mappen die het zal proberen en dat bestand te lokaliseren. In dit geval zorgen we ervoor dat we de code hebben vereisen 'voorbeeld'
dat Ruby het kan vinden omdat het onze zal controleren / Lib /
map en daar zal het bestand worden opgegeven. Dit is een slimme truc die je in veel Ruby-edelstenen zult zien gebruikt, maar het kan behoorlijk verwarrend zijn als je het nog nooit hebt gezien.
Onze applicatiecode zal in een bestand met de naam zijn example.rb
.
Voordat we beginnen met het schrijven van een applicatiecode, onthoud dat we dit project TDD aan het doen zijn. Dus we laten de tests in ons specificatiebestand ons eerst begeleiden over wat te doen.
Laten we beginnen met te veronderstellen dat je gebruikt bewaker
om je tests uit te voeren (dus elke keer dat we een wijziging aanbrengen example.rb
, De bewaker zal de wijziging opmerken en doorgaan met rennen example_spec.rb
om ervoor te zorgen dat onze tests slagen).
Voor ons om TDD goed te doen, onze example.rb
bestand zal leeg zijn en dus als we het bestand openen en het opslaan in zijn huidige staat, dan zal Guard worden uitgevoerd en zullen we (niet verrassend) ontdekken dat onze test zal mislukken:
Fouten: 1) RSpecGreeter RSpecGreeter # greet () Fout / fout: greeter = RSpecGreeter.new # Given NameError: niet-geïnitialiseerde constante RSpecGreeter # ./spec/example_spec.rb:5:inblok (2 niveaus) in 'Klaar in 0.00059 seconden 1 voorbeeld, 1 fout Mislukt voorbeelden: rspec ./spec/example_spec.rb:4 # RSpecGreeter RSpecGreeter # greet ()
Laten we, voordat we verder gaan, nogmaals verduidelijken dat TDD gebaseerd is op het uitgangspunt dat elke regel code een reden heeft om te bestaan, dus niet doen begin voorwaarts te racen en schrijf meer code op dan je nodig hebt. Schrijf alleen de minimale hoeveelheid code die nodig is om de test door te geven. Zelfs als de code lelijk is of niet voldoet aan de volledige functionaliteit.
Het punt van TDD is om een krapte te hebben terugkoppeling, ook bekend als 'rood, groen, refactor'). Wat dit in de praktijk betekent is:
Je zult in een oogwenk zien dat omdat onze eisen zo eenvoudig zijn, we niet hoeven te refactiveren. Maar in een echt project met veel complexere vereisten, zult u waarschijnlijk de derde stap moeten nemen en de code die u hebt ingevoerd, moeten aanpassen om de test te laten slagen.
Terugkomend op onze falende test, zoals je kunt zien in de fout, is er geen RSpecGreeter
klasse gedefinieerd. Laten we dit oplossen en de volgende code toevoegen en het bestand opslaan zodat onze tests kunnen worden uitgevoerd:
klasse RSpecGreeter # code zal uiteindelijk hier eindigen
Dit resulteert in de volgende fout:
Fouten: 1) RSpecGreeter RSpecGreeter # greet () Fout / fout: greeter = greeter.greet # When NoMethodError: undefined methodgreet 'voor # # ./spec/example_spec.rb:6:in' block (2 niveaus) in 'Klaar in 0,00036 seconden 1 voorbeeld, 1 fout
Nu kunnen we zien dat deze fout ons de methode vertelt begroeten
bestaat niet, dus laten we het toevoegen en dan ons bestand opnieuw opslaan om onze tests uit te voeren:
klasse RSpecGreeter def greet # code zal uiteindelijk hier eindigen
OK, we zijn er bijna. De fout die we nu krijgen is:
Fouten: 1) RSpecGreeter RSpecGreeter # greet () Fout / fout: begroeter = groet.moet gelijk zijn ('Hallo RSpec!') # Vervolgens verwacht: "Hallo RSpec!" got: nil (vergeleken met behulp van ==) # ./spec/example_spec.rb:7:in 'block (2 niveaus) in' Klaar in 0.00067 seconden 1 voorbeeld, 1 fout
RSpec vertelt ons dat het verwachtte te zien Hallo RSpec!
maar in plaats daarvan kreeg het nul
(omdat we de begroeten
methode, maar heeft in feite niets gedefinieerd in de methode en dus komt het terug nul
).
We voegen het resterende stuk code toe waarmee onze test kan slagen:
klasse RSpecGreeter def greet "Hallo RSpec!" einde
Daar hebben we het, een passerende test:
Afgewerkt in 0,00061 seconden 1 voorbeeld, 0 storingen
We zijn hier klaar. Onze test is geschreven en de code is geslaagd.
Tot nu toe hebben we een testgestuurde ontwikkelingsproces toegepast om onze applicatie te bouwen, naast het gebruik van het populaire RSpec-testraamwerk.
We laten het hier voorlopig nog liggen. Kom terug en sluit je aan bij deel twee, waar we meer RSpec-specifieke functies zullen bekijken en de Pry-edelsteen gebruiken om je te helpen bij het debuggen en schrijven van je code.