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.

No comments:

Post a Comment