Imparare Ruby col TDD

Allora, devo imparare Ruby un po’ meglio di quanto i miei primi esperimenti mi abbiano insegnato (negli scorsi mesi ho fatto in modo di sostituire gli scripts unix e ANT che usavo con degli scripts di Ruby, per imparare il linguaggio).

Ho quindi pensato di sviluppare un altro scriptino, ma questa volta configurabile a tal punto da richiedere un deciso approccio ad oggetti : si tratta di generare una tabella per la JDepend fixture dello zio Bob (http://www.butunclebob.com/ArticleS.UncleBob.JdependFixture). Questa fixture è proprio simpatica e permette di imporre che alcuni packages non siano dipendenti da altri, e rileva le ciclicità appoggiandosi su JDepend (http://clarkware.com/software/JDepend.html).

Trovo però che sia molto pesante dover andare a scrivere quei tabelloni, che, se io ho 12 unità di rilascio, sono grossi 12×12. Voglio quindi creare una classe in Ruby che, opportunamente configurata, crei questi tabelloni per me, e me li stampi in un file o magari in console, così che io possa fare un bel copia incolla sul mio fitnesse. In più voglio poter definire delle “librerie di base”, cioè delle unità di rilascio che non devono avere collegamenti efferenti verso alcuna altra unità da me rilasciata, ma che possono essere usate da chiunque, magari anche meglio, voglio poter definire programmaticamente quali dipendenze mi attendo.

Ecco un esempio di quello che intendo creare :
!|Module Dependencies|sample|
||.aspects|.client|.grouping|.factories|.baselib|
|.aspects|||||X|
|.client|||||X|
|.grouping|X||||X|
|.factories|X|X|||X|
|.baselib||||||

Inizio quindi con lo scrivere un test di ruby per ottenere la prima linea, che ha “!|Module Dependencies|” fisso e poi la root. Per lanciare i tests uso un profilo di run di Eclipse, in particolare “Test::Unit” che mi mette a disposizione il plugin rubyeclipse (http://rubyeclipse.sourceforge.net/).

require ‘test/unit’
require ‘dependencer’

class DependenciesTableTest < Test::Unit::TestCase

@tableMaker = Dependencer.new

def setup
@tableMaker.addRoot(“sample”)
end

def testFirstRowContainsRootPackage
assert_equal(“!|Module Dependencies|sample|”,@tableMaker.printHeader)
end

end

Run, manca la classe Dependencer, e la aggiungo :

class Dependencer
end

Run. E già il test mi dice che non capisco nulla di Ruby : Exception: undefined method `addRoot’ for nil:NilClass

A quanto pare setup viene eseguito prima delle inizializzazioni delle variabili d’istanza, quindi metto l’inizializzazione del Dependencer (nome terribile, lo so :) ) dentro al setup.
def setup
@tableMaker = Dependencer.new
@tableMaker.addRoot(“sample”)
end

Run… manca il metodo addRoot. Aggiungo in Dependencer :
@root

def addRoot(root)
@root = root
end

Run. Manca printHeader. Aggiungo in Dependencer con la mia interpretazione javista di una concatenazione di stringhe :
def printHeader
return “!|Module Dependencies|” + @root + “|”
end

Run. Verde! Ruby è decisamente intuitivo nella sua sintassi di base.

Refactoring… direi nulla, non c’è nemmeno un if.

Prossimo test, se non c’è una root il risultato di printHeader dovrebbe essere : !|Module Dependencies|
def testFirstRowContainsJustTheFixture
assert_equal(“!|Module Dependencies|”,@tableMaker.printHeader)
end

Run. Exception: cannot convert nil into String
Comprensibile, @root non è inizializzata, e io ci butto dentro la soluzione più stupida che ci sia :
def printHeader
if @root != nil
return “!|Module Dependencies|” + @root + “|”
else
return “!|Module Dependencies|”
end
end

Run. Verde! Ha funzionato di nuovo! Devo dire che ruby è davvero intuitivo.
Refactoring…
1 – duplicazione della costante “!|Module….”
Per questo estraggo la costante
2 – if
Qui ci metterei volentieri un NullObject pattern, ma il vantaggio, visto che l’if per adesso è solo uno, sarebbe davvero minimo, e quindi lo lascio così, per adesso.

Ma come si faranno le costanti con Ruby?? Chiediamo a google. Ah, ecco, basta iniziare con una maiuscola :
FIXTURE_NAME = “Module Dependencies”
…..
def printHeader
if @root != nil
return “!|” + FIXTURE_NAME + “|” + @root + “|”
else
return “!|” + FIXTURE_NAME + “|”
end
end

A questo punto devo ottenere la prima riga, quella dei nomi dei packages : ||.aspects|.client|.grouping|.factories|.baselib|
Notare però che il punto serve solo se è settata la root. Quindi per ora voglio :
def testTitlesRowContainsAllPackages
assert_equal(“||aspects|client|grouping|factories|baselib|”,@tableMaker.printPackagesRow)
end

e nel setup :
def setup
@tableMaker = Dependencer.new
@tableMaker.addPackage(“aspects”)
@tableMaker.addPackage(“client”)
@tableMaker.addPackage(“grouping”)
@tableMaker.addPackage(“factories”)
@tableMaker.addPackage(“baselib”)
end

Run. Manca il metodo addPackage. Qui voglio una lista… ah, pare che si chiami Array, che in Ruby sono a lunghezza variabile. E per aggiungere un elemento al fondo dell’array? Naah, questo è davvero ganzo :
def initialize
@packages = Array.new
end
….
def addPackage(package)
@packages << package
end

Quelle doppie freccette aggiungono un elemento al fondo dell’array! Bellezza dell’overloading degli operatori.
Notare la initialize, Ruby, a differenza di Java, pare che non inizializzi le variabili di instanza se non dentro a un metodo esplicito, per fortuna la initialize viene invocata automaticamente. E io che credevo che fosse solo questione di setup di TestCase!

Run. Manca “printPackagesRow”.
Proviamo con questo :
def printPackagesRow
row = “|”
for package in @packages
row += “|” + package
end
row += “|”
end

Run. Verde! Continuo ad azzeccarci al primo colpo, giuro! Che poi sono stupefatto, mi sono appena reso conto di aver dimenticato i returns… sarà una delle tante cose che Ruby “intuisce”. Ma preferisco essere esplicito e aggiungo il return.

Però se non ci sono packages settati non è che ho dei guai? Io voglio che ritorni la colonnina vuota :
def testNoPackagesReturnsFirstEmptyColumn
emptyTableMaker = Dependencer.new
assert_equal(“||”,emptyTableMaker.printPackagesRow)
end

Run. Verde di nuovo. Ma non è che questi unit test di ruby sono truccati? ;)

Passiamo alle cose difficili, così magari avrò dei rossi.
Se la root è presente ci vogliono i punti!
def testRootIsPresentThusPackagesAreDotted
@tableMaker.addRoot “sample”
assert_equal(“||.aspects|.client|.grouping|.factories|.baselib|”,@tableMaker.printPackagesRow)
end

Run. Rosso, oh là, mancano i punti. Un altro if entra in gioco!

def printPackagesRow
row = “|”
for package in @packages
row += “|”
if @root != nil
row += “.”
end
row += package
end
row += “|”
end

Run. Verde. Ok, ma questo root inizia a essere fastidioso. E’ ora di rifattorizzare per il NullObject direi. Forse in Ruby c’è un modo ancora più furbo, ma ne so troppo poco per ora e mi attengo alla tradizione. Definisco due classi :

class Root
@root

def initialize(root)
@root = root
end

def dot
return “.”
end

def to_str
return @root + “|”
end
end

class NoRoot
def dot
return “”;
end
def to_str
return “”;
end
end

Entrambe hanno il metodo “dot” e fanno l’override della funzione to_str (che sarebbe la toString)

Cambio di conseguenza il Dependencer per sfruttare queste due classi :

class Dependencer

FIXTURE_NAME = “Module Dependencies”

@root
@packages

def initialize
@root = NoRoot.new
@packages = Array.new
end

def addRoot(root)
@root = Root.new(root)
end

def addPackage(package)
@packages << package
end

# printHeader è davvero dimagrita!
def printHeader
return “!|” + FIXTURE_NAME + “|” + @root
end

#E anche printPackagesRow è molto più snella!
def printPackagesRow
row = “|”
for package in @packages
row += “|” + @root.dot + package
end
row += “|”
end

end

Run. Verde, refactoring ok.

Ora passiamo alla costruzione delle righe interne, testo la prima :

def testFirstRowIsCorrectlyFormatted
assert_equal(“|aspects||||||”,@tableMaker.printRow(0))
end

Run. Manca printRow, manco a dirlo.

def printRow(row)
emptyColumns = “|”*@packages.size
return “|”+@packages[row]+emptyColumns
end

Qui in Java avrei probabilmente usato una for, ma con Ruby certe cose sono molto più compatte, notare la linea in grassetto.

Run. Rosso : Test::Unit::AssertionFailedError: <“|aspects||||||”> expected but was <“|aspects|||||”>.

Manca un trattino! (Ne ho generati cinque in coda ad aspects, mentre invece dovevo generarne sei)

Piccola modifica, con un magic number…. brrrr

def printRow(row)
emptyColumns = “|”*(@packages.size + 1)
return “|”+@packages[row]+emptyColumns
end

Run. Verde.
Refactor? Mah, al massimo ci potrei estrarre un metodo printEmptyColumns, per dare un po’ più di significato a quel +1, ma per adesso lascio stare. Mi viene però il sospetto che Dependencer stia assumendo un po’ troppe responsabilità, va bene la generazione di header e titolo della tabella, ma forse le righe dovrebbero essere generate dai rispettivi packages. Mi tengo questo pensiero da parte.

Provo con la seconda riga :
def testSecondRowIsCorrectlyFormatted
assert_equal(“|client||||||”,@tableMaker.printRow(1))
end

Ho un po’ barato, perchè ottengo subito un verde.. eh sì, in teoria per far passare il test della prima riga mi bastava scrivere :
def printRow(row)
emptyColumns = “|”*(@packages.size + 1)
return “|”+@packages[0]+emptyColumns
end

Ma non esageriamo con l’ingenuità dell’implementazione, se no questo post diventa chilometrico davvero.

Ora provo a generare la pagina completa (questo sarebbe un po’ il mio test di acceptance, ma è anche un test unitario per creare il metodo printWholeTable) :
def testWholeTableIsGeneratedCorrectly
assert_equal(“!|Module Dependencies|\n” +
“||aspects|client|grouping|factories|baselib|\n” +
“|aspects||||||\n” +
“|client||||||\n” +
“|grouping||||||\n” +
“|factories||||||\n” +
“|baselib||||||\n”,@tableMaker.printWholeTable);
end

Run. Rosso, manca printWholeTable. Implementazione banale :

def printWholeTable
table = printHeader + “\n”
table += printPackagesRow + “\n”
for i in 0..@packages.size – 1
table += printRow(i)+”\n”
end
return table
end

Run. Verde. Si però quella for è davvero brutta, in particolare “@packages.size -1” non mi piace, pensare che Ruby ha il bellissimo “for … in” che tanto mi piace in Java e tanto mi piace anche qui.

Ora procederò ad aggiungere la possibilità di definire programmaticamente delle relazioni “buone” tra i packages, cioè le “X” nella tabella. Voglio che sia comodo, quindi metodi molto semplici.

Per adesso chiudo qui, a breve la continuazione, in cui credo proprio che dovrò iniziare a far collaborare più di un oggetto.

Intanto prendo il risultato e lo metto in Fitnesse, per vedere se anche per lui è corretto.

Fitnesse acconsente.

Comments

In Ruby, la parola “return” è opzionale. Se manca, il valore di ritorno del metodo è il valore dell’ultima espressione calcolata. Non sono sicuro che mi abituerò mai, ma nei metodi di una sola riga la cosa non mi sembra più tanto anormale – si vede che mi sto rubyzzando.
Posted by Paolo “Nusco” Perrotta on 09/07/2006 08:03:09 AM

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s