Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Rspec stories e rails, realizzare test semantici con semplicità

Costruire le applicazioni a partire da costrutti in linguaggio naturale è quasi realtà
Costruire le applicazioni a partire da costrutti in linguaggio naturale è quasi realtà
Link copiato negli appunti

Quando sviluppiamo un'applicazione (Web o meno) dobbiamo dedicare del tempo a testarne le funzionalità. Esistono decine di tecniche di test diverse, cosi come decine di possibili implementazioni.

Tra queste spicca il Behavior Testing; questa tecnica fa parte di un più corposo stile di programmazione chiamato BDD, o Behavior Driver Development, che cerca di focalizzare l'attenzione dello sviluppatore sugli aspetti di un'applicazione creando in questo modo un flusso di lavoro orientato all'analisi delle macro-caratteristiche funzionali più importanti.

Ad esempio supponiamo di dover sviluppare un'applicazione per la gestione di un autosalone, in particolare stiamo creando la parte per la ricezione delle automobili ordinate dai clienti. In un contesto BDD ecco in che modo porre le specifiche per tale azione:

Scenario: Ricezione di un automobile

Dato un fornitore 
e la presenza in magazzino di tre automobili
Quando il fornitore consegni un automobile
e l'automobile sia priva di difetti
e l'automobile sia del modello richiesto
Allora in magazzino saranno presenti quattro automobili
e al fornitore verrà certificata l'avvenuta consegna

Quanto ho appena scritto viene chiamato in gergo un 'exemplar' e serve ad illustrare il comportamento atteso dal programma a fronte di uno scenario stabilito. Gli Exemplar sono molto comodi in quanto fanno emergere alcuni dettagli di implementazione che altrimenti resterebbero nascosti all'interno di descrizioni troppo generali.

RSpec Stories utilizza gli exemplar come veicolo di testing, come scopriremo più avanti, questa libreria dà la possibilità di associare ad ogni espressione (sentence) un blocco di codice e di utilizzare alcune parole chiave all'interno del testo (Given, When, Then) per stabilire le varie fasi del test (setup, azioni, assunti). Prima di entrare nel dettaglio di quanto appena introdotto vorrei però spendere un capitolo in una veloce introduzione a RSpec in modo da completare questa panoramica sugli strumenti BDD disponibili.

Rspec

David Chelimsky (l'attuale mantainer di RSpec e RSpec Stories) descrive RSpec come: «un DSL, per descrivere il risultato atteso da un particolare sistema utilizzando dati di esempio». Nella pratica RSpec fornisce alcuni metodi molto intelligenti (should, shouldn't, ...) per generare unit test ad alto contenuto semantico, come ad esempio:

@credit_card.should be_valid

In questo caso il contenuto del test è assolutamente percepibile anche a livello semantico (la carta di credito deve essere valida) pur essendo stato scritto in linguaggio Ruby. Questo risultato è possibile grazie al metodo should che viene aggiunto a run-time all'interno della classe Object divenendo quindi accessibile da tutte le istanze dell'applicazione.

Il metodo should accetta come parametro un oggetto di tipo Matcher (nel nostro caso il risultato del metodo be_valid) e certifica il passaggio del test in questione nel caso in cui tale oggetto evocato sull'istanza principale (nel nostro caso @credit_card) dia esito positivo, il che è equivalente a scrivere:

(be_valid).matches?(@credit_card)

be_valid fa invece parte della categoria dei predicati arbitrari; questo metodo infatti non esiste e viene quindi intercettato da un'apposito method_missing che lo interpreta rimuovendo la parte 'be_' e creando un oggetto Matcher che punti al metodo il cui nome sia consistente alla parte rimanente ('valid') con l'aggiunta di un '?' ('valid?').

Questa introduzione sarà necessaria nel proseguio dell'articolo in quanto all'interno di RSpec Stories le clausole di riuscita di un test verranno espresse utilizzando la sintassi appena illustrata.

Che cosa c'è in una Storia?

Come Dan North ha scritto in un suo famosissimo articolo, le Storie (o exemplar come sono stati chiamati più tecnicamente in precedenza) hanno come scopo principale quello di esemplificare le funzionalità del prodotto utilizzando un linguaggio che, pur non essendo tecnico, possa mettere in risalto tutte le criticità di tali flussi operativi.

In questo modo è possibile condividere la stesura e la critica delle Storie create non soltanto con altri sviluppatori ma anche con persone che pur non possedendo requisiti tecnici specifici possano essere comunque considerate esperte all'interno del dominio di appartenenza del problema (es: il commitente, il consulente esterno, etc. ).

Il vantaggio più evidente di questa pratica è misurato da un generale miglioramento della comprensione del progetto da parte non solo del team di sviluppo ma anche dai non addetti ai lavori.

La struttura classica di una Storia non si discosta di molto rispetto all'esempio riportato nel prologo anche se, oltre al fatto che questo tipo di documenti sono normalmente scritti in lingua inglese, vi sono delle (piccole) convezioni da rispettare. Prendiamo per esempio la seguente Storia:

Story: Guest user login
         As a guest
         I want to log-in using my credential information
         So that I can access to my personal Area

Scenario: Log-in with valid credentials
         Given a guest user
         When  he log in with username sandro and password mypass
         Then  flash must contain 'Welcome Sandro'

Il documento si divide in due parti: un'intestazione e uno scenario; mentre l'intestazione è univoca, ogni storia può contenere uno o più scenari. In questo caso l'unico scenario presente ipotizza l'avvenuto login da parte di un'ospite anonimo.

Nell'intestazione sono presenti quattro elementi descrittivi:

  • il titolo della storia (Guest user login)
  • il ruolo con cui approccio a questo aspetto dell'applicativo (As a guest)
  • le azioni che voglio compiere (I want to...)
  • l'obiettivo che voglio raggiungere (So that I...)

Lo scenario è invece caratterizzato da:

  • un titolo che identifichi l'approccio all'aspetto in questione ('log-in with valid...')
  • una serie di predicati che definiscano il contesto iniziale; in questo caso il predicato è uno solo (Given...) ma se dovessero essere di più è sufficiente aggiungere una nuova riga iniziando con And..
  • una serie di predicati per descrivere gli eventi che devono accadere, vale anche qui la stessa strutturazione del punto precedente ma con la particella When
  • una serie di predicati per specificare le conseguenze attese dagli eventi inseriti (la particella da utilizzare in questo caso è Then)

Rispettando queste poche regole ogni storia che definiremo in fase di sviluppo sarà utilizzabile anche con Rspec Stories in fase di testing.

Creiamo un'applicazione di esempio

Ora che abbiamo compreso, almeno da un punto di vista generico, la natura e l'obiettivo di una Storia all'interno dell'approccio BDD possiamo cominciare ad installare e provare Rspec Stories. Per prima cosa creiamo una nostra applicazione di prova:

rails demoapp

Ora eseguiamo tutte le configurazioni del caso (le poche configurazioni in quanto, come sappiamo, uno dei capisaldi di Rails è 'Convention over Configuration') e prepariamo la nostra applicazione installando il plugin RSpec e RSpec on Rails

script/plugin install http://github.com/dchelimsky/rspec.git/
script/plugin install http://github.com/dchelimsky/rspec-rails.git/
script/generate rspec

Completato anche questo passaggio diamo un'occhiata alla struttura che è stata aggiunta alla nostra applicazione. In particolare ci interessiamo dalla cartella demoapp/stories e del suo contenuto:

all.rb
helper.rb 

Troviamo solo due file, se li apriamo noteremo che il primo di essi non fa che includere tutti i file della cartella mentre il secondo richiama alcune librerie (rspec in primis) nonché il file environment.rb della nostra applicazione.

Nelle prossime righe creeremo i tre files che compongono una storia (sempre all'interno della directory stories):

  • login.story: il file contenente la storia in puro testo
  • login.rb: il file che eseguirà la nostra storia
  • steps/login_steps.rb: la corrispondenza tra i predicati della storia e le azioni da eseguire nella nostra applicazione

Come storia di prova possiamo utilizzare quella già visto in precedenza, copiamola quindi all'interno del file login.story; per quanto riguarda invece il contenuto del file launcher possiamo utilizzare il seguente codice, che non fa altro che richiamare helper.rb e invocare la storia da eseguire in associazione con il file login_steps.rb:

require File.join(File.dirname(__FILE__), *%w[helper])
require File.join(File.dirname(__FILE__), *%w[steps login_steps])

with_steps_for :login_steps do
  run File.dirname(__FILE__) + "/login.story"
end

Nel file login_steps.rb dovremo invece definire manualmente la corrispondenza tra le tre azioni specificate in login.story e quanto deve invece accadere nella nostra applicazione:

steps_for :login do
Given "a guest user" do 
  session[:current_user] = nil
end
When "he log in with username $username and password $password" do |username,password|
  post '/login', :username => username, :password=>password
end
Then "flash must contain '$message'" do |message|
  flash[:notice].should == message
end
end

Per ogni predicato all'interno dello scenario è stata definita un'azione che "traduce" quanto scritto in linguaggio naturale nel corrispettivo frammento di codice. Se osserviamo attentamente il secondo o il terzo blocco noteremo che all'interno della frase sono state inserite wildcard, cioè parti che possono assumere un qualsiasi valore (ad esempio nel secondo predicato sono presenti le wildcard $username e $password.

Nel momento in cui la storia verrà eseguita le due variabili username e password associate a questo blocco verranno valorizzato dei rispettivi valori specificati nella storia.

Da segnalare è anche la sottile differenza che intercorre fra i tre blocchi, che si traduce in una più generale differenza di approccio alla creazione del codice a seconda che stiamo costruendo una corrispondenza per un predicato Given, When o Then:

  • Given: fase di setup, nella maggior parte dei casi queste azioni valorizzeranno qualche variabile del sistema
  • When: fase dispositiva, queste azioni tenderanno a simulare la navigazione o ad eseguire particolari routine
  • Then: fase di valutazione delle conseguenze, in queste azioni è necessario avvalersi della logica booleana (nella maggior parte dei casi utilizzando RSpec) per testare se il comportamento atteso si è verificato o meno

Lanciamo l'applicazione

Per eseguire quanto appena creato apriamo una shell (o un prompt dei comandi), portiamoci nella root dell'applicazione ed eseguiamo:

ruby stories/login.rb

Se tutto va bene dovremmo ricevere un rapporto nel quale ci viene spiegato che lo scenario non è stato eseguito correttamente, questo è naturale in quanto al momento la nostra applicazione manca dell'aspetto (behavior) che stiamo testando.

In un classico scenario BDD si dovrebbe ora procedere alla creazione di controllers e actions necessari al corretto funzionamento dell'aspetto descritto, in questo caso l'argomento esula da quello di questo articolo e quindi non proseguirò oltre.

Conclusioni

La tecnica appresa in questo articolo offre numerosissimi vantaggi, in primo luogo fornisce di un metodo per approcciare in modo coerente sia alla parte di sviluppo che a quella di testing, in secondo luogo ci dà la possibilità di delegare la stesura delle Stories anche a persone senza una specifica formazione informatica. Inoltre la necessità di creare nuovi predicati (e quindi di stendere nuovi blocchi di codice) diminuirà sempre più col tempo in quanto quelli già esistenti andranno a costiture una base di dati sufficientemente estesa a coprire moltissimi diversi scenari.

Ti consigliamo anche