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.

No comments:

Post a Comment