Monday, July 13, 2009

Pharo et tests unitaires (4): 1 Trait acheté, 4 tests offerts !!

Dernière partie de la série:
Smalltalk ajoute à l'héritage un autre mécanisme de réutilisation de code, les Traits. Pour (sur-)simplifier, un Trait fournit un ensemble de méthodes définissant un comportement. Plusieurs classes peuvent utiliser un même Trait et une classe peut être composée de plusieurs Traits. Cela nous offre un moyen d'éviter des duplications de code en mutualisant des fonctions ou comportements génériques entre les classes.

On peut faire l'analogie avec les modules de Ruby (appelés aussi mixins). Par exemple, le module Observable implémente le design pattern du même nom. Si une classe inclut ce module, elle possède de fait les méthodes add_observer, notify_observers, ... Si cette même classe doit aussi être Singleton, il suffit de lui inclure le module en question.

Voici un exemple extrêmement scientifique:
require 'observer'
require 'singleton'

class Agathe
include Observable
# Agathe est unique
include Singleton

def reveille_toi
# ça peut mettre un peu de temps 
sleep 10*rand

changed true
notify_observers
end
end

class Laurent
# Laurent est unique aussi ;)
include Singleton

def initialize
Agathe.instance.add_observer self
  end

def update
puts "C'est pas trop tôt"
end

def reveille_agathe
Agathe.instance.reveille_toi
end
end

Laurent.instance.reveille_agathe

et le résultat:

$ ruby test_modules.rb
C'est pas trop tôt

Pour plus de détails sur les Traits de Smalltalk, vous pouvez consulter le document Traits: Composable Units of Behaviour(Nathanel Schärli, Stéphane Ducasse, Oscar Nierstrasz, Andrew P.Black).

La manière d'utiliser les Traits pour les tests unitaires dans Pharo m'a interpellé.

On écrit de nombreux tests unitaires de base qui se ressemblent, surtout lorsqu'on manipule des listes, des collections. Si on reprends notre exemple d'une classe qui représente une collection de films (Movies), on écrit à peu près les tests suivants:
  • pour une instance de Movies toute fraîche:
    • size retourne 0
    • isEmpty retourne true
  • j'ajoute un film (via Movies#add)
    • size retourne 1
    • isEmpty retourne false
  • j'ajoute trois films
    • size retourne 3
  • je retire un film
    • ....
C'est sympa les premières fois mais la pratique devient vite répétitive. Heureusement, certains frameworks permettent de réduire ce genre de tests à une simple ligne de code (avec Shoulda dans le monde Rails par exemple).

Dans Pharo, des Traits dédiés à faciliter l'écriture des tests ont été écrits. On peut le voir en parcourant le package Collection-Tests. Les Traits sont par convention préfixés de la lettre T.







1. Test de la classe Movies avec le Trait TSizeTest

Créons notre champ d'expérimentation en ajoutant la classe MoviesWithTraitsTest:

Object subclass: #MoviesWithTraitsTest
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Movies'

Si vous avez bien suivi le premier billet sur les tests d'une Collection, nous testons la méthode Movies#size avec une collection vide, puis contenant un et plusieurs éléments. Et ça tombe bien, le Trait que TSizeTest existe.



Indiquons à notre classe de test d'utiliser ce Trait. Modifiez la définition de la classe comme suit:

Object subclass: #MoviesWithTraitsTest
uses: TSizeTest
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Movies'

et auto-magiquement, les méthodes du Trait sont ajoutées dans MovieWithTraitsTest.



Etudions la méthode MovieWithTraitsTest#test0TSizeTest:

test0TSizeTest
self shouldnt: [self empty] raise: Error.
self shouldnt: [self sizeCollection] raise: Error.
self assert: self empty isEmpty.
self deny: self sizeCollection isEmpty.


Les deux première lignes vérifient que les méthodes MovieWithTraitsTest#empty et MovieWithTraitsTest#sizeCollection ne lancent pas d'exception.

D'une part, les Traits ne peuvent définir de variables. D'autre part l'utilisateur du Trait a le devoir d'implémenter certaines méthodes pour le bon fonctionnement ce ce Trait.

Nous pouvons le vérifier en ouvrant MovieWithTraitsTest#sizeCollection:

sizeCollection
"Answers a collection not empty"
    ^ self explicitRequirement


Bon, allons examiner explicitRequirement pour en connaître les détails. Clic droit sur la méthode et sélectionnez implementors.



Cela nous amène à la définition de Object#explicitRequirement:

explicitRequirement
self error: 'Explicitly required method'

qui lance explicitement une jolie erreur.

Nous devons donc implémenter MovieWithTraitsTest#empty et MovieWithTraitsTest#sizeCollection. Commençons par la première en retournant une instance de Movies vide:

empty
^ Movies new

et une instance de Movies avec quelques éléments pour la suivante:

sizeCollection
"Answers a collection not empty"
    | movies |
movies := Movies new.
#('Amélie' 'Alien 4' 'Délicatessen') do: [:aTitle|
movies add: (Movie newWithTitle: aTitle)].
^ movies

et lançons les tests. Le debugger se manifeste:




MovieWithTraitsTest#test0TSizeTest vérifie le retour de la méthode Movies#isEmpty qui n'existe pas. Créons la méthode:



isEmpty
^ moviesCollection isEmpty



puis relançons les tests. C'est maintenant le test MovieWithTraitsTest#testSize qui s'arrête car Movies#do: aBlock n'est pas implémentée. Appuyons nous toujours sur moviesCollection:


do: aBlock
moviesCollection do: aBlock 


et puis tout va beaucoup mieux.




2. Notre propre Trait

Allons plus loin en implémentant notre propre Trait de test. Pour reprendre la démarche du billet précédent, nous allons créer des tests vérifiant que Movies#includes: aMovie retourne true pour les instances de Movie qu'il contient, false dans le cas contraire.

Le test à false est plus simple à réaliser. Voici la méthode MoviesWithTraitsTest#testIncludesElementNotInReturnsFalse:
testIncludesElementNotInReturnsFalse
self deny: (self sizeCollection includes: self elementNotIn).
elementNotIn représente une instance qui n'est pas censée se trouver dans sizeCollection. Comme les Traits ne possèdent pas de variables d'instances, nous passons par une méthode:
elementNotIn
^ Movie newWithTitle: 'City Of Lost Children'.
Vérifiez le bon fonctionnement des tests, cela doit passer comme une lettre à la poste (quand il n'y a pas grève).

Passons au test opposé:
testIncludesElementInReturnsTrue
self assert: (self sizeCollection includes: self elementIn).
Cette fois-ci, MoviesWithTraitsTest#elementIn doit retourner une instance de Movie précédemment ajoutée à la collection retournée par sizeCollection. Nous devons passer par une variable d'instance.
elementIn
^ aVeryLongEngagement
et déclarez aVeryLongEngagement comme variable d'instance.




Initialisez ensuite cette variable dans la méthode setUp:
setUp
aVeryLongEngagement := Movie newWithTitle: 'A Very Long Engagement'

et nous pouvons ajouter l'instance dans MoviesWithTraitsTest#sizeCollection:
sizeCollection
"Answers a collection not empty"
| movies |
movies := Movies new.
#('Amélie' 'Alien 4' 'Délicatessen') do: [:aTitle|
movies add: (Movie newWithTitle: aTitle)].
movies add: aVeryLongEngagement.
^ movies

Ceci fait, nos tests passent.



3. Création du Trait


Nous allons maintenant extraire nos deux méthodes de test vers un nouveau Trait. Faites un clic droit sur la méthode testIncludesElementInReturnsTrue et sélectionnez move to trait...



Pharo demande le Trait dans lequel déplacer la méthode. Nous créons un nouveau Trait, cliquez *New Trait*:



puis choisissez lui un joli nom, ici TCollectionIncludesTest:



et votre nouveau Trait est né.



Si vous revenez à la définition de la classe MoviesWithTraitsTest, vous remarquerez qu'elle utilise notre Trait:



Celui-ci s'appuie sur deux méthodes: sizeCollection et elementIn:
testIncludesElementInReturnsTrue
self assert: (self sizeCollection includes: self elementIn).
elles sont destinées à être implémentées par les classes qui utilisent le Trait. Autant le rendre explicite. Rajoutons les méthodes suivantes à TCollectionIncludesTest:

sizeCollection
"Answers a collection not empty"
^ self explicitRequirement


elementIn
"Answers an instance included in self sizeCollection"
^ self explicitRequirement


Ajoutons enfin MoviesWithTraitsTest#testIncludesElementNotInReturnsFalse au Trait:



et complétons par la méthode elementNotIn:

elementNotIn
"Answers an instance not included in self sizeCollection"
^ self explicitRequirement


Voilà, le Trait peut être utilisé par d'autres classes.

4. Avantages et inconvénients.

L'utilisation des Traits apporte les avantages suivants:
  • la cohérence: un Trait définit un comportement, et les tests utilisant des Traits testeront ces mêmes comportements. Cela garantit que les mêmes noms de méthodes sur des objets de type différent aboutiront à un comportement similaire. On peut vérifier les conventions.
  • réutilisation: la mutualisation des méthodes de test évite les duplications de code pour tester des comportements similaires.

Ceci dit des effets de bord apparaissent. Si la granularité des Traits n'est pas assez fine, on se retrouve obligé à implémenter beaucoup de méthodes d'un coup. Par exemple, lorsque nous avons utilisé TSizeTest, nous avons dû implémenter Movies#do: et Movies#isEmpty, méthodes qui ne nous intéressaient pas forcément. Un Trait avec plus de tests nous aurait obligé à implémenter encore plus de méthodes. Cela peut casser la démarche de développement piloté par les tests.

Les Traits de test sont un atout, mais je pense important:
  • de bien étudier un Trait avant de l'utiliser
  • de garder des Traits avec un périmètre de test limité

Sunday, July 5, 2009

Pharo et tests unitaires (3): les collections, seconde partie

Episode 3, saison 1.
Résumé de l'épisode précédent:

Inspector Thomas D. Derrick was facing Mister M., a supposed movies dealer. He had to find the truth. What did Mister M. do with the movies ?

- Where do you put the movies ? T.D.Derrick asked.
- I'm a Collection. I keep them.
- You're a Collection, aren't you ? So what's your size ?
- Well ... Zero now. I'm empty !!
- I'm sure you lie. Take this movie ... "Star Wars". Take it !
- OK OK, done.
- And tell me what's your size now ...
- One. You see, I have it.

These right answers did not disturbed T.D.Derrick.

- Play again with me. Add these movies. "Blade Runner" and ... this one, "Alien".
- Done.
- And now, your size ?
- Three. Can I go now ?


Mister M. seemed to be a real Collection. T.D.Derrick felt he was missing something. Then a subtil smile appeared on his face:

- Do you include Star Wars ?
- Err ... hard to tell. I don't understand.
- What ?
- I don't understand "include"
- Yep, I got you now !!
- Damned ...

Au dernier billet, j'ai décrit l'implémentation, pilotée par les tests, de la méthode Movies#size. La méthode retourne le nombre de films ajoutés via Movies#add.


Néanmoins, Movies utilise un compteur et ne passe pas par une Collection interne pour stocker les instances de Movie. Les tests nous ont permis de développer la solution la plus simple possible.

1. Test: Movies#includes: aMovie

Ajoutons un autre test pour notre collection: Movies#includes: aMovie pour une instance de Movie précédemment ajoutée retourne vrai, sinon faux.

testIncludesAnAddedMovieReturnsTrue
| movies starWars bladeRunner alien |
movies := Movies new.

starWars := Movie newWithTitle: 'Star Wars'.
bladeRunner := Movie newWithTitle: 'Blade Runner'.
alien := Movie newWithTitle: 'Alien'.

movies add: starWars; add: bladeRunner.        

self assert: (movies includes: starWars).
self assert: (movies includes: bladeRunner).
self deny:  (movies includes: alien).

Notez l'appel self deny : si la méthode TestCase#assert vérifie que l'expression en paramètre retourne true, TestCase#deny passe si l'expression testée retourne false.

Lancez le test. Le debugger s'arrête car Movies#includes:aMovie n'existe pas.

Comme vu au billet précédent, utilisez le bouton Create Method pour la déclarer.



Cette fois-ci, on doit vraiment passer par une collection interne.
includes: aMovie
^ moviesCollection includes: aMovie


Déclarons moviesCollection comme variable d'instance pour la rendre accessible aux autres méthodes de la classe Movies.



Comme toute variable d'instance, elle doit être initialisée. Au hasard, utilisons une instance d'OrderedCollection. Editez Movies#initialize:

initialize
super initialize.
size:=0.
moviesCollection:=OrderedCollection new

En relançant le test, le debugger indique que la première assertion ne passe pas. En utilisant la fonctions watch it sur (movies includes: starWars), le retour est en effet false.












Utilisons un autre outil de debuggage. Sélectionner movies, puis clic-droit, inspect it.




La fenêtre qui apparaît permet de visualiser l'état de l'instance Movies. En cliquant sur size, on voit que la méthode retourne 2. Cela semble logique, nous avons ajouté les instances "Star Wars" et "Blade Runner".



Regardons moviesCollection. Nous avons bien une instance d'OrderedCollection, mais vide (rien entre les parenthèses).



Pour s'en assurer, utilisons l'éditeur de la fenêtre pour demander la taille de moviesCollection. Tapez

moviesCollection size

puis utilisez la fonction inspect it.





On voit que size retourne bien 0 (une instance de la classe SmallInteger).

Les instances de Movie ne sont donc pas ajoutées à moviesCollection. Examinons la méthode Movies#add:
add: aMovie
size := size + 1

En effet ... modifions la méthode:
add: aMovie
size := size + 1.
moviesCollection add: aMovie

et maintenant le test passe !




2. Refactoring.


2.1 movies, starWars, bladeRunner et alien

Supprimons les duplications. Avant toute chose, vérifions que tous les tests de notre package Movies passent.

Les duplications se trouvent dans MoviesSizeTest. Les instances de Movie pour "Star Wars", "Blade Runner" et "Alien" sont créées dans les méthodes testWithOneMovieSizeReturnsOne, testWithThreeMoviesSizeReturnsThree et testIncludesAnAddedMovieReturnsTrue. De plus, chacune des méthode de test instancie Movies.

TestCase propose les méthodes setUp et tearDown pour ce genre de situation. TestCase#setUp est exécutée avant chaque test, et tearDown aprés chaque test. Lorsqu'on exécute tous les tests de MoviesSizeTest, l'appel des méthodes est le suivant (à l'ordre des tests près):
  1. setUp
  2. testIncludesAnAddedMovieReturnsTrue
  3. tearDown
  4. setUp
  5. testNewInstanceSizeReturnsZero
  6. tearDown
  7. setUp
  8. testWithOneMovieSizeReturnsOne
  9. tearDown
  10. setUp
  11. testWithThreeMoviesSizeReturnsThree
  12. tearDown
Nous allons mettre en commun la création des instances de Movie dans la méthode setUp. Définissez MoviesSizeTest#setUp comme suit:

setUp
movies := Movies new.
starWars := Movie newWithTitle: 'Star Wars'.
bladeRunner := Movie newWithTitle: 'Blade Runner'.
alien := Movie newWithTitle: 'Alien'.<br />

Les quatre variables sont des variables d'instances. Adaptez ensuite chaque méthode de MoviesSizeTest en vérifiant que les tests passent à chaque fois.


testNewInstanceSizeReturnsZero
self assert: movies size = 0



testWithOneMovieSizeReturnsOne
movies add: starWars
self assert: movies size = 1



testWithThreeMoviesSizeReturnsThree
movies add: starWars;
add: bladeRunner;
add: starWars.
self assert: movies size = 3



testIncludesAnAddedMovieReturnsTrue
movies add: starWars;
add: bladeRunner.
self assert: (movies includes: starWars).
self assert: (movies includes: bladeRunner).
self deny:  (movies includes: alien).

2.2 Renommer MoviesSizeTest

Le nom de la classe ne reflète plus assez la totalité des tests, car on vérifie les retours des méthodes Movies#size et Movies#includes. Le vrai objectif est de s'assurer que Movies#add ajoute bien les instances de Movie. Renommons la classe en MoviesAddMovieTest.


Pour ce faire, clic-droit sur la classe et sélectionnez rename.

Saisissez MoviesAddMovieTest comme nom de classe et validez.


2.3 Supprimer le comteur size de Movies

Regardons Movies#add de nouveau:
add: aMovie
size := size + 1.
moviesCollection add: aMovie

moviesCollection est une instance d'OrderedCollection.

OrderedCollection#size retourne le nombre d'instance stockées. On peut donc supprimer le compteur size qui fait doublon.

add: aMovie
moviesCollection add: aMovie

Relancez les tests:





Oups ... On s'aperçoit via le debugger que Movies#size retourne toujours 0.






L'erreur saute aux yeux en ouvrant Movies#size:

size
^ size

Corrigeons la méthode:

size
^ moviesCollection size

et les tests repassent !

Nettoyons au passage Movies#initialize pour supprimer l'initialisation de size:
initialize
super initialize.
moviesCollection:=OrderedCollection new

et nous pouvons finalement supprimer size des variables d'instances de Movies:

Object subclass: #Movies
instanceVariableNames: 'moviesCollection'
classVariableNames: ''
poolDictionaries: ''
category: 'Movies'

3. Pause publicité

Notre collection est minimale mais testée. Comme pour les billets précédents, nous écrivons d'abord le test puis nous nous laissons guider par les erreurs et le debugger pour implémenter le code fonctionnel.

Après chaque test, on modifie le code testé et de test pour l'améliorer.

La prochaine fois, nous retravaillerons MoviesAddMovieTest mais en utilisant les Traits, un mécanisme de réutilisation de code entre classes.

Wednesday, July 1, 2009

Pharo et tests unitaires (2): les collections, première partie

En route pour le second billet de notre trilogie... qui devient au moins une quadrilogie (à vouloir trop en dire). Un petit point pour voir où nous en sommes dans la série:
La programmation d'un logiciel implique généralement l'écriture de nombreuses collections. D'autant plus que les bonnes règles de design nous conseillent que chaque collection soit encapsulée dans sa propre classe. Nous manipulons ainsi des primitives de plus haut niveau.

Au passage, je conseille de lire l'essai "Object Calisthenics" de Jeff Bay paru dans "The ThoughtWorks Anthology". Jeff Bay y décrit neuf étapes aboutissant à un meilleur design, dont cette pratique sur les collections.


Si on écrit beaucoup de collections, cela implique de coder de nombreux tests de ces collections. Toutefois, on arrive assez facilement à implémenter ce genre de test et cela me semble donc un bon moyen de se familiariser avec le développement piloté par les tests.
Dans notre cas, nous avions précédemment créé la classe Movie. Nous allons maintenant écrire une collection Movies (notez le s) pour recenser des instances de Movie.

Nous aimerions connaître le nombre d'instances de Movie ajoutées à notre collection, via la méthode Movies#size. Voici les tests que nous pouvons écrire:
  1. pour une instance de Movies toute fraîche (donc vide): size retourne 0
  2. j'ajoute un film (via Movies#add): size retourne 1
  3. j'ajoute trois films:  size retourne 3
1. Test d'une instance Movies vide

Créons d'abord la classe MoviesSizeTest qui hérite TestCase:

TestCase subclass: #MoviesSizeTest
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Movies'

puis ajoutons notre premier test:

testNewInstanceSizeReturnsZero
    | movies |
    movies := Movies new.
    self assert: movies size = 0

Acceptez la méthode (Ctrl-s). Comme vu dans les billets précédents, Pharo va vous proposer de déclarer la variable movies, puis la classe Movies.

Lancez alors le test; le debugger s'arrête car nous n'avons pas redéfinit la méthode size.



Rajoutons la méthode Movies#size en appliquant un principe fondamental: "Do the simplest thing that could possibly work":
size
    ^ 0
relancez le test et savourez un instant ce sentiment de domination totale....


Pourquoi faire passer le test de manière aussi "stupide" ?
  • rappelez-vous que stupide est un compliment pour un programme ;) (Keep It Simple, Stupid) : le code reste le plus compréhensible possible.
  • on voit de manière flagrante que ce seul test est insuffisant.
  • on peut déjà archiver un travail testé.
  • on ressens la satisfaction d'un premier test qui passe, cela entretient la motivation.
De plus, ces premiers tests simples permettent de s'assurer de la présence de tous les éléments de base, ici la classe Movies et la méthode size (ce qui est un bon début).

2. Test d'une instance Movies contenant un film

Nous allons maintenant ajouter une instance Movie à notre collection et vérifier que size retourne 1. Voici le code de notre test:

testWithOneMovieSizeReturnsOne
    | movies starWars |
    movies := Movies new.
   
    starWars := Movie newWithTitle: 'Star Wars'.   
    movies add: starWars.
   
    self assert: movies size = 1 

A l'exécution du test, le debugger s'arrête car la méthode Movies#add n'existe pas. Profitons en pour étudier quelques outils. Sur la droite du debugger, un bouton Create Method permet de créer directement la méthode manquante.



Pharo nous demande de choisir une catégorie pour notre méthode. Pour l'instant choisissez as yet unclassified, nous verrons par la suite qu'il existe une bonne fonction de paresseux pour classer les méthodes :). La méthode créée, le debugger reprends l'exécution et s'arrête dessus:





Nous voyons qu'un modèle de méthode a été enregistré, à nous de le modifier pour l'accorder à nos besoins. Vous pouvez saisir le "stupide" code suivant directement dans l'éditeur du debugger:







add: aMovie 
    size := 1





A l'acceptation, choisissez de déclarer size en variable d'instance.






Continuez le test (Proceed), le debugger s'arrête car l'assertion ne passe pas. On s'attends à ce que Movies#size retourne 1. Voyons ce que la méthode retourne actuellement. Sélectionnez movies size dans le code du debugger, clic-droit, choisissez watch it.


Une fenêtre s'ouvre et nous indique ce que retourne size.


En effet, nous n'avons pas modifié l'implémentation de Movies#size pour utiliser notre nouvelle variable d'instance. Editez la méthode:
size
    ^ size


et continuez le test.

Victoire !!


3. .... ou presque

Relançons tous les tests du package Movies pour vérifier que tout est bon. Sélectionnez le package dans la première colonne du Class Browser puis lancez les tests.

Enfer et damnation:




MoviesSizeTest#testNewInstanceSizeReturnsZero ne passe plus, Movies#size ne retourne pas 0. Comme vu précédemment, utilisez la fonction watch it pour afficher ce que retourne movies size.



Aie, nous n'aurions pas omis d'initialiser une certaine variable par hasard ?
Spécifions l'initialisation de la classe Movies pour mettre sa variable d'instance à 0.  Dans la classe Movies, ajouter la méthode:

initialize
    super initialize.
    size:=0

puis relancez les tests.

C'est mieux.


4. Test d'une instance Movies avec trois films

Continuons en ajoutant trois films à notre instance de Movies et vérifions que Movies#size retourne 3:

testWithThreeMoviesSizeReturnsThree
   | movies starWars bladeRunner alien |
   movies := Movies new.
 
   starWars := Movie newWithTitle: 'Star Wars'.
   bladeRunner := Movie newWithTitle: 'Blade Runner'.
   alien := Movie newWithTitle: 'Alien'.        
 
   movies add: starWars; add: bladeRunner; add: alien.
   self assert: movies size = 3

Ce test ci ne devrait pas poser trop de problèmes. En le lançant, le debugger s'arrête car l'assertion ne passe pas. Après une petite vérification de movies size, nous voyons que size retourne 1. En effet, en ouvrant le code de Movies#add:

add: aMovie 
    size := 1

Modifiez le code comme suit:

add: aMovie 
    size := size + 1

et tout est sous contrôle.




5. Refactoring

Il est temps d'introduire un autre principe du développement piloté par les tests.

Les tests nous permettent de vérifier que nous n'avons pas cassé le fonctionnement de nos objets; nous pouvons alors prendre tout le loisir de jouer (si si, c'est un jeu) avec notre code pour l'améliorer.

Généralement, nous cherchons durant cette phase les duplications de code. Car nos maîtres l'ont dit: Don't Repeat Yourself !


Au passage, le sujet est bien traité dans Pragmatic Programmers: From Journeyman to Master par Dave Thomas et Andy Hunt.

Le refactoring concerne bien sûr le code testé, mais aussi notre code de test ! En pratique, lorsque les choses sont bien faites, le code fonctionnel teste notre code de test et vice-versa.


Une duplication se trouve dans la méthode MoviesSizeTest#testWithThreeMoviesSizeReturnsThree:

   starWars := Movie newWithTitle: 'Star Wars'.
   bladeRunner := Movie newWithTitle: 'Blade Runner'.
   alien := Movie newWithTitle: 'Alien'.        

Améliorons le code en utilisant les jolies possibilités que nous procure Smalltalk:

testWithThreeMoviesSizeReturnsThree
   | movies |
    movies := Movies new.
        
    #('Star Wars' 'Blade Runner' 'Alien') do: [:title| 
        movies add: (Movie newWithTitle: title)].                

  self assert: movies size = 3

Pour être plus clair sur l'instanciation de Movie, on peut utiliser une variable locale au block:

    #('Star Wars' 'Blade Runner' 'Alien') do: [:title| 
        |aMovie|
        aMovie:= Movie newWithTitle: title.
        movies add: aMovie].

A chaque modification, assurez vous que les tests passent encore. Amusez-vous a explorer les possibilités de Smalltalk dans votre nouveau bac à sable, c'est fait pour ça. Si jamais vous avez besoin de revenir en arrière à une version qui fonctionnait, cliquez sur le bouton versions en haut à droite du Class Browser.



L'historique de la méthode apparaît et vous pouvez reprendre une version antérieure en cliquant sur revert.

6. Interlude

Si Movies#size fonctionne comme voulu, tests à l'appui, la fonction de stockage d'instances de Movie reste à réaliser.

Comme l'article deviens long, nous verrons ceci dans une seconde partie.

Nous avons vu les différentes étapes du TDD (Test Driven Development):
  • identifier une fonctionnalité
  • écrire le test
  • faire passer le test le plus rapidement possible
  • améliorer le code pour supprimer les duplications, simplifier, documenter, ...
Nous avons mis en œuvre quelques outils parmi la multitude que Pharo offre pour nous accompagner dans cette démarche.



Pour finir, les exemples sont fortement inspirés de l'ouvrage "Test Driven Development: A Practical Guide" de Dave Astels.