Atelier iOS à SoftShake 2011

badge softshake speakerJ’anime un ateliersur la mise en place d’une usine logicielle à SoftShake lundi après midi.Cet atelier montrera comment mettre en place un projet XCode, faire du TDD avec OCUnit, mettre en place le repo Git; faire un makefile, l’intégrer dans Jenkins et déployer Over the Air via une
Si vous voulez pouvoir coder en même temps sur votre machine , voici les pré-requis.Pas de pré-requis pour participer en auditeur.Pour coder et faire du TDD
- Mac OS X 10.6
- XCode 4
- un peu connaître Objective-CPour la partie intégration avec le repo Git et le makefile
- XCode 4 avec les outils en ligne de commande (dev tools ou system tools selon les versions)
- rake avec les gems json et plist (sudo gem install json plist)
- git et gitx
- avoir un compte github ou un autre repo git accessible en lecture
par tous (pour le CI)
- si possible cloner https://github.com/ValtechTechno/ios-software-factoryLe reste de l’usine (Jenkins, Mobile Store) est trop long a mettre en place en atelier. Tous les éléments de l’usine logicielle et des informations de mise en oeuvre sont partagés sur le github de Valtech, team iOS https://github.com/ValtechTechno/ios-software-factory sous licence Apache 2. Les pré-requis sont dans les readme.

La présentation se trouve ici.

 

Réflexions sur un problème de conversion d’indicateurs

Je fais pas mal de Groovy ces temps-ci pour exploiter des résultats de test de temps de réponse relevés en unitaire avec Silk Performer. Les rapports générés par l’outil dans ce cadre ne sont pas directement utilisables dans ce cadre. J’ai donc un certain nombre de scripts qui convertissent un rapport en XML en un rapport en HTML assorti de calculs assez basiques, quelques courbes et une historisation des données. Rien de bien compliqué.

Et puis je suis tombée sur un problème qui m’a donné un peu plus de fil à retordre. Sa résolution m’a fait penser à des discutions lors au coding dojo sur le fait que TDD fait émerger naturellement la solution. Vous verrez que ma réponse est "ça dépend" ;-).

Ce post est aussi l’occasion de montrer comment du code évolue pas à pas et comment on peut arriver à diverses solutions avec le même processus. J’ai fait les exemples en Kata pour qu’ils puissent être suivis pas à pas.

S’équiper pour la route

Cet article est fait pour que vous puissez expérimenter vous aussi même si vous ne maîtrisez pas bien Groovy. De toute manière, je ne suis pas une experte non plus, j’ai commencé il y a quelques semaines.

Pour ces examples, on va voyager léger.

  • Groovy : dézipper quelque part, mettre à jour le path,
    REM groovySetup.cmd
    Set GROOVY_HOME=W:\noinstall_prg\engines\groovy-1.7.2
    Set Path=%GROOVY_HOME%\bin;%Path%
  • un éditeur qui a le support Groovy (Eclipse, JEdit et sûrement quelques autres)

Jusque là ça va ?

Allez au travail, votre premier code Groovy ! On prend son éditeur et on tape ça :

//helloworld.groovy
println "Hello World"
>groovy HelloWorld.groovy
Hello World

Et Voilà !

Toujours là ?

On ne va quand même pas travailler sans tests donc passons au premier test unitaire. On peut faire des tests unitaires de classe avec des TestCase JUnit, mais lorsqu’on est en mode scripting il y a bien plus simple :

// HelloWorldWithTest.groovy
assert false
>groovy HelloWorldWithTest.groovy
Caught: Assertion failed:

assert false

        at HelloWorldWithTest.run(HelloWorldWithTest.groovy:2)

Passons à un test un peu plus utile : Phase 1 du TDD "faire planter le test"

// HelloWorldWithTest.groovy
s = "xxx"
assert "Hello World" == s
>groovy HelloWorldWithTest.groovy
Caught: Assertion failed:

assert "Hello World" == s
                     |  |
                     |  xxx
                     false

        at HelloWorldWithTest.run(HelloWorldWithTest.groovy:4)

assert précise les valeurs de toutes les variables et des comparaisons

Et enfin Phase 2 "le code qui passe le test avec succès" :

// HelloWorldWithTest.groovy
s = "Hello World"
assert "Hello World" == s
>groovy HelloWorldWithTest.groovy

Voilà vous êtes officiellement capable de faire du Groovy et prêts à passer à la résolution du problème.

Le problème à résoudre

Silk Performer permet de définir des SLA, des seuils maximaux par type de métrique. Par exemple on peut appliquer un seuil pour le temps de réponse de toutes les actions utilisateur, ou la taille des pages d’une action utilisateur donnée. En pratique il y a deux seuils appelés Bound1 et Bound2. On peut attacher des messages d’alerte à ces seuils, mais pour l’exploitation ultérieure ces messages sont trop textuels. Il est plus simple de repartir des compteurs.

Pour reporter des indicateurs Success, Warning et Error, et il faut convertir des nombres de relevés inférieurs à chaque seuil en nombres de relevés en statut Success, Warning, Error.

Après quelques retraitements, chaque mesure ressemble à ça :

     <Measure>
       <Name>TOppMgmtHome</Name>
       <Class>Timer</Class>
       <Type>Response time[s]</Type>
       <Bound1>3.000000000</Bound1>
       <Bound2>6.000000000</Bound2>
       <CountBelowBound1>1</CountBelowBound1>
       <CountBelowBound2>1</CountBelowBound2>
       <CountMeasured>1</CountMeasured>
       <Value>0.484000000</Value>
       <Unit>Seconds</Unit>
     </Measure>

Les informations qui vont nous intéresser sont les 2 Bounds et les 3 Counts.

  • Bound1 indique le seuil de déclenchement de l’alerte le plus bas
  • Bound2 indique le seuil de déclenchement de l’alerte le plus élevé
  • CountMeasured affiche le nombre de mesures relevées
  • CountBelowBound1 affiche le nombre de mesures qui sont inférieures au seuil 1. Elles seront aussi inférieures au seuil 2, c’est logique mathématiquement
  • CountBelowBound2 affiche le nombre de mesures qui sont inférieures au seuil 2
  • pour compliquer les choses, les seuils (l’un ou l’autre) ne sont pas obligatoires, et dans ce cas aucune des mesures n’est inférieure au seuil. Dans ce cas l’élément Bound est là et contient 0
  • pour finir, après avoir mise au point le code, je me suis rendue compte que non vérifié ne voulais pas forcément dire Success. Un statut Unchecked a été ajouté.

A partir de ces spécifications, j’ai d’abord analysé tous les cas pour faire le jeu de test et dans la foulée produit un code assez court basé sur les règles que j’avais constaté. Puis je me suis dis que c’était un bon test pour du TDD. Comme cette solution initiale est très compacte, elle à la fin du post, en chute.

Attention âmes sensibles, vous allez voir du code moche, très moche. Mais c’est aussi en partie le but de l’expérience.

Et si je faisais comme ça sinon je ferai autrement

Allez, baby steps en TDD, prenons le premier cas. Si je ne fais aucune vérification, j’aurai toutes les mesures en Unchecked.

//IfElseStyle.groovy
assert [1,0,0,0] == computeTestStatus(1,0,0,0,0) // no bounds

J’ai besoin d’une liste de compteurs par status en sortie. [1,0,0,0] correspond respectivement à Unchecked, Success, Warning, Error. Les valeurs que je passe à computeTestStatus sont respectivement

  • c pour CountMeasured
  • b1 pour Bound1
  • b2 pour Bound2
  • cbb1 pour CountBelowBound1
  • cbb2 pour CountBelowBound2

ça ne compile pas et c’est normal, le code n’est pas là

>groovy IfElseStyle.groovy
Caught: groovy.lang.MissingMethodException: No signature of method: IfElseStyle.
computeTestStatus() is applicable for argument types: (java.lang.Integer, java.l
ang.Integer, java.lang.Integer, java.lang.Integer, java.lang.Integer) values: [1
, 0, 0, 0, 0]
        at IfElseStyle.run(IfElseStyle.groovy:2)

Juste pour voir le test échouer (je suis dans la phrase vérifier que le test échoue du TDD).

//IfElseStyle.groovy
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->

}
assert [1,0,0,0] == computeTestStatus(1,0,0,0,0) // no bounds

… et assert qui montre que le test d’égalité échoue. Il affiche à droite la valeur du résultat, qui est null à ce point

>groovy IfElseStyle.groovy
Caught: Assertion failed:

assert [1,0,0,0] == computeTestStatus(1,0,0,0,0) // no bounds
                 |  |
                 |  null
                 false

        at IfElseStyle.run(IfElseStyle.groovy:5)

Et la version la plus simple qui passe le test même si elle est un peu stupide :

//IfElseStyle.groovy
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  [1,0,0,0]
}
assert [1,0,0,0] == computeTestStatus(1,0,0,0,0) // no bounds

Un refactoring pour clarté

all_unchecked = [1,0,0,0]
assert all_unchecked == computeTestStatus(1,0,0,0,0) // no bounds

Je ne monterai plus les étapes "faire échouer", "passer le test", "refactorer" systématiquement, c’est un peu lourd à copier/coller ;-)

Ajout d’un deuxième test, avec un warning sur le seuil 1. Donc le nombre de mesures au dessous du seuil est 0.

//IfElseStyle.groovy
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  if (b1>0)
    [0,0,1,0]
  else
    [1,0,0,0]
}
all_unchecked = [1,0,0,0]
assert all_unchecked == computeTestStatus(1,0,0,0,0) // no bounds 
got_a_warning = [0,0,1,0]
bound1 = 2
assert got_a_warning == computeTestStatus(1,bound1,0,0,0) // b1

Ok. Continuons dans le cas par cas. On verra bien ce que ça donne et si un pattern émerge

//IfElseStyle.groovy
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  if (b1>0)
    if (cbb1>0)
      [0,1,0,0]
    else
      [0,0,1,0]
  else
    [1,0,0,0]
}
all_unchecked = [1,0,0,0]
assert all_unchecked == computeTestStatus(1,0,0,0,0) // no bounds 
got_a_warning = [0,0,1,0]
bound1 = 2
assert got_a_warning == computeTestStatus(1,bound1,0,0,0) // b1 
all_success = [0,1,0,0]
assert all_success == computeTestStatus(1,bound1,0,1,0) // b1

J’ai quand même noté un truc en faisant mes jeux de test. Si je prend seulement le cas où j’ai un warning, j’ai une symétrie

  • Success: [0,0,1,0] -> 1,b1,b2,0,0
  • Warning: [0,1,0,0] -> 1,b1,b2,1,0

La deuxième colonne, Success, évolue comme ccb1 et la troisième, Warning, à l’inverse. Qu’est ce qui se passe si j’ai plusieurs mesures ?

  • 2 Success: [0,2,0,0] -> 2,b1,b2,2,0
  • 1 Success et 1 Warning: [0,1,1,0] -> 2,b1,b2,1,0
  • 2 Warnings: [0,0,2,0] -> 2,b1,b2,0,0

La deuxième colonne, Success, évolue comme ccb1 et la troisième, Warning, évolue comme la différence entre c et cbb1. Un refactoring et voyons ce que ça donne.

//IfElseStyle.groovy
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  if (b1>0)
    [0,cbb1,c-cbb1,0]
  else
    [1,0,0,0]
}
...
got_two_warnings = [0,0,2,0]
assert got_two_warnings == computeTestStatus(2,bound1,0,0,0)
got_a_warning_out_of_two = [0,1,1,0]
assert got_a_warning_out_of_two == computeTestStatus(2,bound1,0,1,0)
got_two_success = [0,2,0,0]
assert got_two_success == computeTestStatus(2,bound1,0,2,0)

Après quelques tours de danse, tous les tests sont vérifiés. Passons à un cas avec des erreurs. On ne change pas une équipe qui gagne. C’est fondamentalement le même principe.

//IfElseStyle.groovy
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  if (b1>0)
    [0,cbb1,c-cbb1,0]
  else if (b2>0)
    [0,cbb2,0,c-cbb2]
  else
    [1,0,0,0]
}
...
got_an_error = [0,0,0,1]
bound2 = 3
assert got_an_error == computeTestStatus(1,0,bound2,0,0)
assert all_success == computeTestStatus(1,0,bound2,0,1)
got_an_error_out_of_two = [0,1,0,1]
assert got_an_error_out_of_two == computeTestStatus(2,0,bound2,0,1)

Maintenant qu’on a des vérifications de seuil 1 et 2, il ne reste plus qu’à régler le problème des deux … ensembles …

Et là ça a fait Plouff!!! J’ai rajouté un if/else de plus, ce code est de plus en plus immonde. Le l’ai regardé pendant 10 bonnes minutes et je ne vois pas comment avancer à partir d’ici, à part rajouter encore des couches de if pour avancer test par test. Ce code m’ennuie et ça n’est pas la bonne méthode, je le sais depuis le début.

//IfElseStyle.groovy
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  if (b1>0 && b2>0) {
    //Plouff !!!
  } else if (b1>0)
    [0,cbb1,c-cbb1,0]
  else if (b2>0)
    [0,cbb2,0,c-cbb2]
  else
    [1,0,0,0]
}

Une pause thé pour trouver une idée et je reviens pour un autre essai.

A la mode programmation fonctionnelle

En revenant de ma pause thé de 1m30, j’ai décidé de le refaire en programmation fonctionnelle. Je ne suis pas une experte non plus. Pas sûre que ça sera un modèle, mais au moins ça va m’amuser et j’apprendrai quelque chose.

Posons nous et réfléchissons un peu. Si je ne veux éviter ces forêts de if il faut que je calcule isolément chaque statut. Je verrai après comment éviter de recalculer les mêmes valeurs.

nbUnchecked = { c, b1, b2, cbb1, cbb2 -> 1 }
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  [nbUnchecked(c, b1, b2, cbb1, cbb2), 0, 0, 0]
}

all_unchecked = [1,0,0,0]
assert all_unchecked == computeTestStatus(1,0,0,0,0)

J’ai sauté l’étape du [1,0,0,0] pour vérifier quelque chose de plus intéressant. nbUnchecked est une des fonctions qui va calculer le statut. Pour le moment c’est un fake et elle sert à valider le principe de la construction de la liste.

Ajoutons un cas de warning :

nbUnchecked = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?0 :1
}
nbWarning = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?1 :0
}
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  [nbUnchecked(c, b1, b2, cbb1, cbb2), 0,
   nbWarning(c, b1, b2, cbb1, cbb2), 0]
}

all_unchecked = [1,0,0,0]
assert all_unchecked == computeTestStatus(1,0,0,0,0)  

got_a_warning = [0,0,1,0]
bound1 = 2
assert got_a_warning == computeTestStatus(1,bound1,0,0,0)

J’essaie de faire abstraction de ce que j’ai déjà appris lors de l’essai précédent. nbUnchecked est réécrite pour passer le test. S’il y a une valeur de seuil 1, la mesure n’est pas unchecked sinon elle l’est. L’implémentation de nbWarning est aussi conjoncturelle.

Toujours avec un seuil 1 mais en succès

nbUnchecked = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?0 :1
}
nbWarning = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?c-cbb1 :0
}
nbSuccess = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?cbb1 :0
}
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  [nbUnchecked(c, b1, b2, cbb1, cbb2),
   nbSuccess(c, b1, b2, cbb1, cbb2),
   nbWarning(c, b1, b2, cbb1, cbb2), 0]
}

all_unchecked = [1,0,0,0]
assert all_unchecked == computeTestStatus(1,0,0,0,0)  

got_a_warning = [0,0,1,0]
bound1 = 2
assert got_a_warning == computeTestStatus(1,bound1,0,0,0)
all_success = [0,1,0,0]
assert all_success == computeTestStatus(1,bound1,0,1,0)

Il y a plusieurs situations où le seuil 1 a un valeur, donc il faut un peu plus rentrer dans le détail. La règle implémentée provient de l’analyse faite précédemment en construisant le jeu d’essai.

Il reste à placer la dernière méthode et à faire un peu de refactoring. On utilise un langage fonctionnel on va en profiter.

nbUnchecked = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?0 :1
}
nbSuccess = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?cbb1 :0
}
nbWarning = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?c-cbb1 :0
}
nbError = { c, b1, b2, cbb1, cbb2 ->
  0
}

computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  status = []
  [nbUnchecked, nbSuccess, nbWarning, nbError].each {
    status.add(it.(c, b1, b2, cbb1, cbb2))
  }
  status
}

all_unchecked = [1,0,0,0]
assert all_unchecked == computeTestStatus(1,0,0,0,0)  

got_a_warning = [0,0,1,0]
bound1 = 2
assert got_a_warning == computeTestStatus(1,bound1,0,0,0)
all_success = [0,1,0,0]
assert all_success == computeTestStatus(1,bound1,0,1,0)

Les closures nbUnchecked, nbSuccess,nbWarning, nbError sont mises dans une liste et appelées à tour de rôle. it représente la fonction.

Suite des tests pour plusieurs mesures. Tout passe.

Les seuils d’erreur maintenant. Comme prévu le premier test échoue puisque la closure est un fake.

...
nbError = { c, b1, b2, cbb1, cbb2 ->
  b2>0 ?c-cbb2 :0
}
...
got_an_error = [0,0,0,1]
bound2 = 3
assert got_an_error == computeTestStatus(1,0,bound2,0,0)

Oooups ça ne passe toujours pas. nbChecked est incomplète.

>groovy Fonctionnel.groovy
Caught: Assertion failed:

assert got_an_error == computeTestStatus(1,0,bound2,0,0)
       |            |  |                     |
       [0, 0, 0, 1] |  [1, 0, 0, 1]          3
                    false

        at Fonctionnel.run(Fonctionnel.groovy:40)

Et voilà …

nbUnchecked = { c, b1, b2, cbb1, cbb2 ->
  (b1>0 || b2>0) ?0 :1
}

Grrrrr … le test suivant échoue :o. Le calcul du nombre de Success est un peu simplet et ne marche plus s’il y n’y a que seuil 2.

>groovy Fonctionnel.groovy
Caught: Assertion failed:

assert all_success == computeTestStatus(1,0,bound2,0,1)
       |           |  |                     |
       [0, 1, 0, 0]|  [0, 0, 0, 0]          3
                   false

        at Fonctionnel.run(Fonctionnel.groovy:41)

Et ça se complique. Le nombre de Success est cbb1 s’il y a seuil 1, soit cbb2 s’il y un seuil 2, soit 0 s’il n’y a pas de seuil. Je ne veux pas repartir dans des if. Cherchons autre chose. Si on retourne le problème, le nombre de Success, est ce qui reste de c quand on a enlevé les warnings et les erreurs. Sauf qu’il faut encore traiter le cas il n’y a pas eu de vérification. Au moins, il n’y a plus qu’un if

nbSuccess = { c, b1, b2, cbb1, cbb2 ->
  nw = nbWarning(c, b1, b2, cbb1, cbb2)
  ne = nbError(c, b1, b2, cbb1, cbb2)
  (b1>0 || b2>0) ?(c - nw  -ne) :0
}

Pas extraordinaire, mais ça fonctionne. Le côté par très joli, c’est qu’on doit recalculer 2 fois les nombres d’erreurs et déterminer à plusieurs endroits s’il y a une vérification.

Une version un peu retravaillée qui évite de tester s’il y a des vérifications à plusieurs endroit et aussi de faire des calculs, puisque s’il n’y a pas de vérification le résultat est forcément [1,0,0,0]. J’ai aussi supprimé les parenthèses inutiles sur le add.

nbWarning = { c, b1, b2, cbb1, cbb2 ->
  b1>0 ?c-cbb1 :0
}
nbError = { c, b1, b2, cbb1, cbb2 ->
  b2>0 ?c-cbb2 :0
}
nbSuccess = { c, b1, b2, cbb1, cbb2 ->
  nw = nbWarning(c, b1, b2, cbb1, cbb2)
  ne = nbError(c, b1, b2, cbb1, cbb2)
  c - nw  - ne
}
computeTestStatusChecked = { c, b1, b2, cbb1, cbb2 ->
    status = [0]
    [nbSuccess, nbWarning, nbError].each {
      status.add it(c, b1, b2, cbb1, cbb2)
    }
    status
}
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
( b1>0 || b2>0) ?computeTestStatusChecked(c, b1, b2, cbb1, cbb2)  :[c,0,0,0]
}

Pour le recalcul des nombres de warning et d’erreurs c’est l’ordre d’appel qui pose problème. Gros refactoring pour appeler les fonctions dans le bon ordre et une seule fois.

nbFailure = { c, b, cbb ->
  b>0 ?c-cbb :0
}
computeTestStatusChecked = { c, b1, b2, cbb1, cbb2 ->
  nbWarning = nbFailure(c, b1, cbb1)
  nbError = nbFailure(c, b2, cbb2)
  nbSuccess = c - nbWarning - nbError
  [0, nbSuccess, nbWarning, nbError]
}
computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
(
  b1>0 || b2>0) ?computeTestStatusChecked(c, b1, b2, cbb1, cbb2)  :[c,0,0,0]
}

Dernier test a passer pour le cas seuil 2 uniquement avec plusieurs mesures et on passe au cas où les deux seuils sont vérifiés, le cas qui a fait Plouff!! plus haut. Est ce que ça se passera mieux cette fois ci ?

assert all_success == computeTestStatus(1,bound1,bound2,1,1)
assert got_a_warning == computeTestStatus(1,bound1,bound2,0,1)

Jusque là tout va bien ! … pas pour longtemps.

assert got_an_error == computeTestStatus(1,bound1,bound2,0,0)
>groovy
Fonctionnel.groovy
Caught: Assertion failed:

assert got_an_error == computeTestStatus(1,bound1,bound2,0,0)
       |            |  |                   |      |
       [0, 0, 0, 1] |  [0, -1, 1, 1]       2      3
                    false

        at Fonctionnel.run(Fonctionnel.groovy:40)

D’où vient ce nombre négatif ? Du fait que la fonction nbFailure (comme nbWarning et nbError qu’elle remplace) compte en réalité les franchissements de seuil. Les mesures qui franchissent le seuil 2 franchissent aussi le seuil 1. Elles sont comptées en Warning et en Erreur, ce qui ne se voyait pas avant car on testait chaque cas isolément.

computeTestStatusChecked = { c, b1, b2, cbb1, cbb2 ->
  nbError = nbFailure(c, b2, cbb2)
  nbWarning = Math.max(0, nbFailure(c, b1, cbb1) - nbError)
  nbSuccess = c - nbWarning - nbError
  [0, nbSuccess, nbWarning, nbError]
}

Il faut retirer le nombre d’erreurs du nombre de warnings, mais uniquement s’il y a des warnings sinon le résultat devient négatif.

On est bientôt au bout. Il reste à vérifier le résultat pour plusieurs mesures avec les vérifications des deux seuils.

got_two_errors = [0,0,0,2]
got_a_warning_and_an_error = [0,0,1,1]
assert got_two_success == computeTestStatus(2,bound1,bound2,2,2)
assert got_a_warning_out_of_two == computeTestStatus(2,bound1,bound2,1,2)
assert got_two_warnings == computeTestStatus(2,bound1,bound2,0,2)
assert got_an_error_out_of_two == computeTestStatus(2,bound1,bound2,1,1)
assert got_two_errors == computeTestStatus(2,bound1,bound2,0,0)
assert got_a_warning_and_an_error == computeTestStatus(2,bound1,bound2,0,1)

C’est fini, ça fonctionne, et je ne me suis pas trop ennuyée

Première conclusion

Et voilà, on en est venu à bout pas à pas. Le code n’est pas parfait mais il fonctionne. Pas trop pénible non plus à faire.

C’est un des points forts de TDD. En général, on arrivera au bout, avec un résultat plus ou moins joli, même si on n’est pas très inspiré (c’est quand même un des sujets de Kata les moins sexy que j’ai vu).

Pourquoi la deuxième méthode a mieux réussi que la première ?

Je ne suis pas sûre que le langage fonctionnel soit pour grand chose. C’est plutôt le style de programmation qu’il implique qui fait que ça a mieux marché sur cet exemple :

  • Créer des fonctions (ou des closures) m’a obligée à nommer les choses et à mieux identifier les règles que je manipulais
  • Le fait de ne pas vouloir de if parce que ça ne cadrait pas esthétiquement avec le reste du code a évité les plats de spaghetti. Objectivement, ça ne change rien du point du point de vue interne. L’opérateur ternaire ?: fait un if. Mais il garde le code plus compact au prix d’un manque de lisibilité pour certains.
  • La deuxième version part de la structure du résultat. Je veux un indicateur Success -> comment est calculé cet indicateur. Le première version part plutôt de la source de données et de ses caractéristiques. Les données étant complexes, on s’est perdu. Il me semple que TDD marche mieux avec une logique top-down.
Alors est ce que TDD fait émerger la solution naturellement ?

Naturellement dans ce contexte signifiait pour moi que la solution va s’imposer au terme du processus, sans effort, sans heurts. En fait je pense qu’il faut le voir plutôt comme un phénomène naturel dans ce qu’il peut avoir de sauvage, chaotique et inattendu.

La sélection naturelle

En insistant sur la première stratégie on aurait peut être abouti à quelque chose. La solution aurait été très laborieuse et très pénible à mettre au point. Après tout dépend de sa tolérance à la peine.   L’échec de la première tentative peut aussi être vu comme une sélection naturelle. Les solutions trop pénibles sont abandonnées.

La deuxième cause de sélection est le test. Les solutions qui ne passent pas les tests sont éliminées sans discussion.

La solution évolue

Sur l’ensemble des expérimentations que j’ai vu en dojo, il me semble que la solution, la bonne, celle qui est jolie et efficace, ne vient pas forcément tout de suite, "naturellement". J’ai noté que dans les dojos il y a souvent des reprises. Les gens font une version, découvrent un truc, pensent à une autre stratégie, retentent autrement. La conception évolue au fil du temps pour arriver à quelque chose de plus raffiné, dans un processus naturel dans le sens où il le cheminement n’est pas contrôlé, pas conscient. Seuls les mécanismes de construction le sont : mettre en place un test, répondre au problème, revoir.

Réaliser une belle solution nécessite un minimum de réflexion préalable sur l’algorithme à mettre en place. Mais cette réflexion est difficile quand il y a trop d’éléments à appréhender en même temps, qu’on n’a pas de matière à réflexion, quand on ne peut pas expérimenter. L’intérêt de TDD peut être aussi d’apprendre quelque chose sur le problème et de découvrir des informations utiles à la résolution du problème plus tard.

La solution s’adapte

La nature prend aussi parfois des voies différentes pour un même problème. En réalité, j’ai fait le deuxième essai deux fois, à deux jours d’intervalle à cause de l’ajout du statut Unchecked. Je suis arrivée à deux solutions assez différentes selon que le Unchecked était posé au départ ou introduit à la fin. Je suis sûre que si je refais ce Kata encore une fois j’aurais encore une version différentes (j’ai à peu près 8 versions différentes d’un même Kata en R). A chaque tour on apprend quelque chose sur le langage, la syntaxe, les stratégies qui marchent ou pas et ça impacte la solution.

Donc est ce que la solution vient "naturellement" au sens de "facilement", pas toujours. Mais une sélection naturelle et une évolution continue de la conception travaillent en arrière plan pour faire émerger  la solution.

Et la chute alors ?

De prime abord, la situation est un peu compliquée mais finalement les règles auxquelles ont est arrivé au travers d’un processus TDD un peu naif peuvent être déduites assez rapidement de l’énoncé du problème.

Pour rappel :

  • CountMeasured affiche le nombre de mesures relevées
  • CountBelowBound1 affiche le nombre de mesures qui sont inférieures au seuil 1. Elles seront aussi inférieures au seuil 2, c’est logique mathématiquement
  • CountBelowBound2 affiche le nombre de mesures qui sont inférieures au seuil 2
  • les seuils (Bound1 ou Bound2) ne sont pas obligatoires, et dans ce cas aucune des mesures n’est inférieure au seuil.
  • lorqu’il n’y a pas de seuil, le statut est Unchecked.

On peut en déduire qu’il faudra inverser les compteurs.

  • Le nombre d’erreurs est le nombre de mesures moins le nombre de mesures au dessous du seuil 2 sauf dans le cas où il n’y a pas de seuil 2
  • Le nombre de warnings est le nombre de mesures moins le nombre de mesures au dessous du seuil 1 moins le nombre d’erreurs sauf dans le cas où il n’y a pas de seuil 1
  • Le nombre de succès est le nombre de mesures au dessous du seuil 1 s’il y a un seuil 1, au dessous du seuil 2 sinon. On n’a pas à verifier s’il y a un seuil 2. Si on est là c’est qu’il n’y a pas de seuil1. Donc le nombre de mesures au dessous du seuil 2 est bien le nombre de succès. S’il n’y a pas de seuil 2 ce nombre sera 0. Mais comme dans ce cas il n’y a eu aucune vérification, ce ne sont pas des succès de toute manière.
  • Le nombre d’unchecked est tout ce qu’on n’a pas compté dans les catégories précédentes, donc le nombre de mesures moins le nombre de succès, le nombre de warnings, le nombre d’erreurs

Le tableau ci-dessous montre ces résultats sous forme de matrice :

Bound2 Bound1
Non vérifiée Vérifiée
Non vérifiée Unchecked: c

Success: 0

Warning: 0

Error: 0

Unchecked: 0

Success: cbb1

Warning: c-ccb1

Error: 0

Vérifiée Unchecked: 0

Success: cbb2

Warning: 0

Error: c-cbb2

Unchecked: 0

Success: cbb1

Warning: c-ccb1

Error: (c-cbb1)-cbb2

C’est la partie la plus délicate car on est obligé de gérer toutes les règles en même temps. Réaliser ce tableau demande beaucoup de concentration alors que faire les cas de test un par un est une tâche interruptible.

Ce qui nous ennuie vraiment c’est la notion de seuil facultatif. ça multiplie les cas à traiter et c’est ce qui a causé tous ces if dans le premier essai. Si l’on essaie de représenter ça mathématiquement, on peut considérer que l’on a des masques qui modifient les compteurs. Pas de seuil 1 -> pas de warning. Pas de seuil 1 -> pas d’erreur. On réduit le nombre de branches à traiter.

Voici le code auquel je suis arrivée très rapidement en partant des principes que je viens de poser :

computeTestStatus = { c, b1, b2, cbb1, cbb2 ->
  b2mask = (b2>0 ?1 :0)
  b1mask = (b1>0 ?1 :0)
  nbErrors = (c-cbb2)*b2mask
  nbWarnings = (c-nbErrors-cbb1)*b1mask
  nbSuccess = b1>0 ?cbb1 :cbb2
  nbUnchecked = c - nbSuccess - nbErrors - nbWarnings
  [nbUnchecked, nbSuccess, nbWarnings, nbErrors]
}

Ce code passe tous les tests qu’on a défini lors des tests précédents.

Il a aussi été implémenté avec des tests mais avec un idée préalable de comment résoudre le problème. Sur le principe il n’est pas très éloigné du code du deuxième essai. C’est surtout la gestion du if qui diffère. Et il est beaucoup plus court, moins fun, à peu près aussi clair.

Seconde conclusion

Même si on peut êcrire un code qui marche sans passer par une démarche TDD, cela ne remet pas en cause le principe :

  • On ne voit pas toujours le truc, et dans ce cas il faut bien une solution qui permet d’avancer quand même et arriver à une solution
  • J’ai eu de la chance de ne pas être interrommpue. Les deux premières implémentations peuvent se faire avec d’autres contraintes en parallèle et des interruptions parce qu’on avance pas à pas. L’analyse sur papier pour la dernière implémentation a moins de points de repères et on perd vite le fil
  • On aurait pu arriver à une implémentation avec des masques aussi dans les versions précédentes. Il se trouve que le problème a été résolu autrement.

Prochaine étape

La prochaine étape est de tester s’il y a des différences en terme de performance entre toutes ces implémentations.

Je mettrai le code en ligne aussi.

Note pour la prochaine fois, tester dans la console Groovy ou Eclipse, le temps de redémarrage de la JVM à chaque fois est assez frustrant ;-)

Remerciements

WordPress ne permet pas d’embarquer du code d’autres sites dans des frames pour des raisons de sécurité. Il n’est donc pas possible d’utiliser des outils comme PasteBin.com ou Pastie.org. Il existe des plugins mais pas sur un WordPress public. J’ai utilisé un utilitaire qui s’appelle Highlight Code Converter que vous pouvez trouver là http://www.andre-simon.de/ pour encoder les portions de code en HTML. Il supporte un très grand nombre de langages et la gestion des copier/coller en entrée et en sortie est vraiment très pratique pour l’usage présent. Bref merci André Simon.