Comment écrire du code testable Conférence Agile France 2010 Florence CHABANOIS
 
 
Frein ? Direction assistée? Boite de vitesse ? Pneus défectueux ?
 
Tests unitaires Tester une  partie  du produit  Simuler un comportement  différent  de la production EasyMock, Mockito, JMock Pour pouvoir tester  unitairement : Les composants doivent être séparables
Isolation Pour permettre la séparation Externaliser  les dépendances public  Moteur() { reservoirHuile =  new  ReservoirHuilePlein(); } public  MoteurOk(ReservoirHuile reservoirHuile) { this .reservoirHuile = reservoirHuile; } Bannir les dépendances  cachées
Une solution Le Test Driven Development  Given… When… Then Implémentation Refactoring Mais.. 1. Il y a souvent du code déjà existant …  sur lequel il faut poser des tests …  dont le nouveau code dépend
Le TDD ne donne pas l’immunité 2. Le code peut être testé et Classes et méthodes fourre-tout Les tests souffrent Performances (constructeur couteux) Compréhensibilité (tests = spécifications) Le développeur aussi Tests lents Maintenabilité
Leitmotiv Deux lignes de conduite Isolabilité Simplicité
 
Symptômes d’un code intestable Isolabilité Simplicité Classes hyperactives Méthodes chargées Interroger des  collaborateurs Etats globaux Annuaires Blocs statiques Instanciation directe Constructeur cher Mélanger service et valeur Héritage
Ennemis jurés
 
public   class  Dictionnaire { private  Map<String, String> definitions =  new  HashMap<String, String>(); public  Dictionnaire()  throws  IOException { File file =  new  File(&quot;francais.txt&quot;); BufferedReader reader =  new  BufferedReader( new  FileReader(file)); String ligne; while  ((ligne = reader.readLine()) !=  null ) { String[] tableauLigne = ligne.split(&quot;:&quot;); definitions.put(tableauLigne[0], tableauLigne[1]); } } public  String getDefinition(String mot) { return  definitions.get(mot); }
Test (n.m.) Opération destinée à contrôler le bon fonctionnement d'un appareil ou la bonne exécution d'un programme dans son ensemble.
Tester getDefinition() @Test public   void  testGetDefinition()  throws  IOException { Dictionnaire dico =  new  Dictionnaire(); String returnedDefinition = dico.getDefinition(&quot;test&quot;); assertThat (returnedDefinition,  is ( equalTo (&quot;Opération destinée à etc.&quot;))); }
public  Dictionnaire()  throws  IOException { File file =  new  File(&quot;francais.txt&quot;); BufferedReader reader =  new  BufferedReader( new  FileReader(file)); String ligne; while  ((ligne = reader.readLine()) !=  null ) { String[] tableauLigne = ligne.split(&quot;:&quot;); definitions.put(tableauLigne[0], tableauLigne[1]); } } Test très lent Obligé d’avoir un fichier
@Test public   void  testGetDefinition_WhenMotNonTrouve()  throws  IOException { Dictionnaire dico =  new  Dictionnaire(); (…) } @Test public   void  testGetDefinition_WhenMotNonValide()  throws  IOException { Dictionnaire dico =  new  Dictionnaire(); (…) }
Symptômes d’un code intestable Un constructeur cher
Un constructeur trop cher Pourquoi c’est mal  On ne peut PAS éviter d’instancier une classe pour la tester Enlève une veine ( seam ) Test pas isolé Test potentiellement couteux Difficile de simuler un autre comportement Voire plus, si c’est utilisé par d’autres tests
Un constructeur trop cher Signes d’alertes If, switch, loop new  d’objets  Des appels statiques …  En fait autre chose que des assignations d’attributs
Un constructeur trop cher Comment y remédier Méthode init() à appeler après le constructeur Pas de test dessus Un constructeur spécial pour le test Déplacement du problème Extraire dans une autre méthode, qu’on surcharge
Code : Extraction de la méthode public  DictionnairePatche()  throws  IOException { initialize(); } protected   void  initialize()  throws  FileNotFoundException, IOException  { File file =  new  File(&quot;francais.txt&quot;); BufferedReader reader =  new  BufferedReader( new  FileReader(file)); String ligne; while  ((ligne = reader.readLine()) !=  null ) { String[] tableauLigne = ligne.split(&quot;:&quot;); definitions.put(tableauLigne[0], tableauLigne[1]); } }
Test : instanciation d’une sous classe static   class   DictionnairePatchForTest   extends  DictionnairePatche { @Override protected   void   initialize ()  throws  FileNotFoundException, IOException { // nothing } } @Test public   void  testGetDefinition()  throws  IOException { Dictionnaire dico =  new  DictionnairePatchForTest(); String returnedDefinition = dico.getDefinition(&quot;test&quot;); assertThat (returnedDefinition,  is ( equalTo (&quot;Opération destinée à contrôler le bon fonctionnement d'un appareil ou la bonne exécution d'un programme dans son ensemble.&quot;))); }
Un constructeur trop cher Comment y remédier Méthode init() à appeler après le constructeur Pas de test dessus Un constructeur spécial pour le test Déplacement du problème Extraire dans une autre méthode, qu’on surcharge Pas de test dessus
Un constructeur trop cher Signes d’alertes mis à jour If, switch, loop new d’objets  Des appels static … . En fait autre chose que des assignations d’attributs Un constructeur spécial test Init()  Du code spécial test : @VisibleForTesting
Un constructeur trop cher Comment y remédier Méthode init()  Un constructeur spécial pour le test Extraire dans une autre méthode, qu’on surcharge  Comment y remédier mieux Faire des constructeurs relais uniquement Passer les collaborateurs  prêts  en paramètres au lieu de les créer Injection de dépendances Factories
public   class  DictionnaireTestable { private  Map<String, String> definitions =  new  HashMap<String, String>(); public  DictionnaireTestable( Map<String, String> definitions )  throws  IOException { this .definitions = definitions; } } Le constructeur  ne coute plus cher Veine créée : Le code n’est plus « collé »
public   class   DictionnaireFactory  { public   static  Dictionnaire buildFromTextFile()  throws  IOException { Map<String, String> definitions =  new  HashMap<String, String>(); File file =  new  File(&quot;francais.txt&quot;); BufferedReader reader =  new  BufferedReader( new  FileReader(file)); String ligne; while  ((ligne = reader.readLine()) !=  null ) { String[] tableauLigne = ligne.split(&quot;:&quot;); definitions.put(tableauLigne[0], tableauLigne[1]); } return   new  DictionnaireTestable(definitions); } } Séparation des  responsabilités
Principe de responsabilité unique
Principe de responsabilité unique Je cherche sur Internet de quelle matière première j’ai besoin pour en fabriquer J’appelle Air France pour réserver un billet d’avion et aller en chercher en Chine Je demande au service Bureautique de m’en installer un nouveau
Principe de responsabilité unique Et le service Bureautique ? Cherche sur Internet de quelle matière première il a besoin pour en fabriquer Appelle Air France pour réserver un billet d’avion et aller en chercher en Chine Le commande chez son fournisseur
Principe de responsabilité unique Etudes Fournisseur Bureautique
Principe de responsabilité unique Créer le graphe d’objets est une responsabilité à part entière public   class  Moteur { private  ReservoirHuile  reservoirHuile ; public  Moteur() { reservoirHuile =  new ReservoirHuilePlein(); } public   void   demarrer () { // (...) } public   void   signalerManqueHuile () { // (...)  } } Création du graphe d’objets Logique métier
Focus sur demarrer() public   void  demarrer() { Moteur moteur =  new  Moteur(); moteur.demarrer(); BoiteDeVitesse boiteVitesse =  new  BoiteDeVitesse(); boiteVitesse.passerLaPremiere(); Embrayage embrayage =  new  Embrayage(); embrayage.relacher(); Accelerateur accelerateur =  new  Accelerateur(); accelerateur.appuyer(); }
Symptômes d’un code intestable Un constructeur cher Des instanciations directes
Des instanciations directes Pourquoi c’est mal Couplage fort Enlève une veine ( seam ) Test pas isolé Test potentiellement couteux Difficile de simuler un autre comportement
Des instanciations directes Signes d’alertes Des « new » dans une classe autre que Factory ou Builder
Des instanciations directes Comment y remédier Framework de mocks : JMockit, Powermock Comment y remédier mieux Passer les objets nécessaires en paramètres de la méthode Séparer construction du graphe d’objets de la logique métier Injection de dépendances Factories
Symptômes d’un code intestable Un constructeur cher Des instanciations directes Des blocs statiques
Des blocs statiques public   class  Joueur { private   static  Plateau  plateau ; static  { if  (Environnement. IS_DEMO ) { plateau  =  new  PlateauCommercial(); }  else  { plateau  =  new  PlateauDeDemo(); } } public   void  joindre(Partie partie) { } }
Des blocs statiques Pourquoi c’est mal Couplage très fort Pas possible de le remplacer par un mock Ni de le surcharger dans les tests Potentiellement très couteux Effets de bord entre des tests censés être isolés Le test passe, parfois Etat permanent
Des blocs statiques Signes d’alertes Static {} Un test qui ne fonctionne plus au sein d’une suite Comment y remédier Supprimer tous les bloc statiques et introduire des classes Passer les collaborateurs en paramètres au lieu de les créer Injection de dépendances Factories
public   class  JoueurTestable { private  Plateau plateau; public  JoueurTestable( Plateau plateau ) { this .plateau = plateau; } public   void  joindre(Partie partie) { (…) } }
Spring-jeu.xml <bean class= &quot;fr.soat.agileconference2010.blocstatic.JoueurTestable&quot;  id= &quot;joueur1&quot;  scope= &quot;prototype&quot; > <constructor-arg ref= &quot;plateau&quot; ></constructor-arg> </bean> <bean class= &quot;fr.soat.agileconference2010.blocstatic.metier. PlateauCommercial &quot;  id= &quot;plateau&quot;  scope= &quot; singleton &quot; ></bean>
testJoindre() Plateau plateau =  new   PlateauDeDemo (); JoueurTestable joueur =  new  JoueurTestable(plateau); joueur.joindre( new  Partie()); //Verifications
Symptômes d’un code intestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes
Des dynasties de classes ?
 
Des dynasties de classes Pourquoi c’est mal Couplage fort avec classe mère Lenteur Fragilité Tests plus difficiles à maintenir (redondance)
Des dynasties de classes Signes d’alertes Quand le code devient difficile à tester  Quand les tests sont redondants / difficile à maintenir à cause de la classe mère Comment y remédier Utiliser la  composition  pour réutiliser du code Limiter l’héritage aux besoins de  polymorphisme
 
 
Symptômes d’un code intestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux
a = new X().traiter(); b = new X().traiter(); a = b ?
Source :  http://misko.hevery.com/2009/10/07/design-for-testability-talk/
 
On veut poser un test sur l’expresso public   class  MachineACafe { public   void  payer( float  montant){ (…) } public  Expresso preparerExpresso() { (…) } public   void  brancher(){ (…) } }
Test de l’expresso  @Test public   void  testPreparerExpresso() { MachineACafe machineACafe =  new  MachineACafe(); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(),  is ( true )); } Null Pointer Exception
Test de l’expresso @Test public   void  testPreparerExpressoEssai2() { MachineACafe machineACafe =  new  MachineACafe(); machineACafe.setBaseDeDonnees( new  BaseDeDonnees()); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(),  is ( true )); } Null Pointer Exception
Test de l’expresso @Test public   void  testPreparerExpressoEssai3() { MachineACafe machineACafe =  new  MachineACafe(); BaseDeDonnees baseDeDonnees =  new  BaseDeDonnees(); baseDeDonnees.init(&quot;myUrl&quot;, &quot;myLogin&quot;, &quot;myPassword&quot;); machineACafe.setBaseDeDonnees(baseDeDonnees); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(),  is ( true )); } Null Pointer Exception
Test de l’expresso @Test public   void  testPreparerExpressoEssai4() { MachineACafe machineACafe =  new  MachineACafe(); BaseDeDonnees baseDeDonnees =  new  BaseDeDonnees(); baseDeDonnees.init(&quot;myUrl&quot;, &quot;myLogin&quot;, &quot;myPassword&quot;); baseDeDonnees.setNotificateur( new  Notificateur()); machineACafe.setBaseDeDonnees(baseDeDonnees); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(),  is ( true )); } CafeException
Pourquoi CafeException ? public   void  verifierPreconditions() { if  (! robinetActive ()) { final  String erreur = &quot;Vérifier le robinet&quot;; baseDeDonnees.logguerErreur( this , erreur); throw   new  CafeException(erreur); } Hein, quel robinet ?
Pourquoi CafeException ? private   boolean  robinetActive() { Robinet robinet = Robinet. getInstance (); return  ( robinet.estOuvert () && robinet.estConnecte( this )); }
Test de l’expresso @Test public   void  testPreparerExpressoEssai5() { MachineACafe machineACafe =  new  MachineACafe(); final  BaseDeDonnees baseDeDonnees =  new  BaseDeDonnees(); baseDeDonnees.init(&quot;myUrl&quot;, &quot;myLogin&quot;, &quot;myPassword&quot;); baseDeDonnees.setNotificateur( new  Notificateur()); machineACafe.setBaseDeDonnees(baseDeDonnees); Robinet. getInstance ().ouvrir(); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(),  is ( true )); } Ok ! Ok !
Je dev Je dev Je dev Je dev Je dev Je dev Je dev Je dev Mon code Il dev Il dev Il dev COMMIT Il dev Il dev Il dev Il dev Il dev COMMIT Son code
Boom public   void  testPreparerVerreEau_whenDefaultValues() { FontaineAEau fontaine =  new  FontaineAEau(); VerreEau verre = fontaine.preparerVerreEau(); assertThat (verre,  is ( nullValue ())); }} AssertionError : expected NULL « Son code »
Test de l’expresso @Test public   void  testPreparerExpressoEssai6() { MachineACafe machineACafe =  new  MachineACafe(); final  BaseDeDonnees baseDeDonnees =  new  BaseDeDonnees(); baseDeDonnees.init(&quot;myUrl&quot;, &quot;myLogin&quot;, &quot;myPassword&quot;); baseDeDonnees.setNotificateur( new  Notificateur()); machineACafe.setBaseDeDonnees(baseDeDonnees); Robinet. getInstance ().ouvrir(); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(),  is ( true )); Robinet. getInstance ().fermer(); } Ok pour le moment…
Des états globaux Pourquoi c’est mal Mensonge : « il n’y a pas de dépendances. » Méthode statique ou Singleton = dépendance cachée. Pas de veine pour placer un mock Test pas isolé Test potentiellement couteux Difficile de simuler un autre comportement Risque de perturbations avec d’autres tests Etat présumé  Plus longs à lancer Débogage difficile
Des états globaux Signes d’alertes Des singletons Du code static : variable, bloc, méthode …  même un seul !!! « chargement global (global load) » : nombre de variables pouvant être modifiées par un état global Des tests qui fonctionnent seuls mais pas en groupe ou vice versa
Des états globaux Comment y remédier Suppression du final et introduction de setters Isoler le problème dans une autre méthode, qu’on surcharge.  Violation de  l’encapsulation Code brouillé Et peu nettoyable Oubli de  reset Ordre compte Lisibilité
Des états globaux Signes d’alertes mis à jour Des singletons Du code static : variable, bloc, méthode …  même un seul !!! car « chargement global / global load » : le nombre de variables qui peuvent être modifiées par un état global Des tests qui fonctionnent seuls mais pas en groupe ou vice versa Du code spécial test Des setters, reset, init dans les singletons @VisibleForTesting
Des états globaux Comment y remédier réellement Bannir singleton et code static Décliner en classes Injection de dépendances
Symptômes d’un code intestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service
Annuaire de services public  Maison(Locator locator) { porte = locator.getPorte(); fenetre = locator.getFenetre(); toit = locator.getToit(); }
Annuaire de services Pourquoi c’est mal Tromperie  « il n’y a pas de dépendances » « il n’y en a qu’une seule » Application entière à initialiser
Annuaire de services Signes d’alertes « Registry », « context », « locator » Comment y remédier Passer les objets  réellement  utilisés Injection de dépendances
Pollueurs
Symptômes d’un code intestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service Interroger des collaborateurs
 
Avoir des intermédiaires public   void  facturer(Commande commande, Client client) { banqueService.prelever(client.getCompteBancaire(),  commande.getTotal() ); emailService.notifierPrelevement(client.getEmail()); }
Avoir des intermédiaires Pourquoi c’est mal Tromperie :  « on a besoin de Commande et Client » Couplage fort avec l’objet intermédiaire Lisibilité  Débogage plus complexe (exception) Initialisation du test plus complexe
Avoir des intermédiaires Signes d’alertes « context », « environment », « container » Objets passés mais jamais utilisés directement  Plus d’un point  env.getUser().autoconnect(); Dans les tests :  Des mocks qui retournent des mocks Devoir mocker des getters/setters
Avoir des intermédiaires Comment y remédier Appliquer le principe de connaissance minimale (Loi de Demeter) toute méthode M d'un objet O peut uniquement invoquer les méthodes de lui-même ses attributs ses paramètres les objets qu'il crée/instancie Passer directement les objets réellement utilisés
public   void  facturer(CompteBancaire  compte ,  double   montant , String  email ) { banqueService.prelever(compte, montant); emailService.notifierPrelevement(email); }
Initialisation du test avant Client client =  new  Client(); final  CompteBancaire compte =  new  CompteBancaire(); client.setCompteBancaire(compte); final  String email = &quot;toto@email.fr&quot;; client.setEmail(email); Commande commande =  new  Commande(); final   double  total = 20.0; commande.setTotal(total); // When manager.facturer(commande, client);
Initialisation du test après final  CompteBancaire compte =  new  CompteBancaire(); final  String email = &quot;toto@email.fr&quot;; final   double  montant = 20.0; // When manager.facturer(compte, montant, email);
Symptômes d’un code intestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service Interroger des collaborateurs Des classes hyperactives
CommandeManager
Des classes hyperactives Pourquoi c’est mal Classe fourre-tout Peu robuste aux changements Lisibilité Maintenabilité
Des classes hyperactives Signes d’alertes « manager », « utils », « helper » Qu’est ce qu’elle fait? Et Pas évidente à comprendre pour un nouvel arrivant / Pas facile d’avoir en tête ce qu’elle fait en une fois Difficile de trouver un nom à la classe Quand un champ n’est utilisé que par quelques méthodes Beaucoup de champs et/ou collaborateurs Beaucoup de méthodes Méthodes avec peu de rapport les unes les autres Méthodes statiques
Des classes hyperactives Comment y remédier Etapes Identifier les responsabilités de la classe Les nommer Les extraire dans autant de classes Une classe peut avoir le rôle d’orchestrer Comment identifier les responsabilités? Repérer les méthodes qui ne sont utilisées que par un ou quelques champs Repérer les méthodes statiques et les rendre à leur paramètres (ou wrapper de paramètres) listerCommandes(Client client) Regrouper méthodes qui se ressemblent Regrouper les attributs souvent utilisés ensemble
Des classes hyperactives Comment y remédier (suite) Si code legacy  Extraire une classe pour chaque modification / nouvelle fonctionnalité
Des classes hyperactives Comment y remédier (suite) Si code legacy  Extraire une classe pour chaque modification / nouvelle fonctionnalité Imbriquer les collaborateurs A Y Z X W A Y Z X W
Symptômes d’un code intestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service Interroger des collaborateurs Des classes hyperactives Des méthodes trop chargées
Au guichet du Grand Huit public   boolean  laisserPasser(Personne personne) { if  (personne.getAge() > 12 && personne.getTaille() > 1.3 && personne.estEnBonneSante()) { if  ( (personne.getAge() < 18 && personne.estAccompagne()) || (personne.getAge() >= 18)){ facturer(personne); return   true ; } } return   false ; }
Des méthodes trop chargées Pourquoi c’est mal Augmente la complexité des tests Très sensible aux modifications Difficile de comprendre tout de suite le fonctionnement
Des méthodes trop chargées Signes d’alertes Si ça dépasse l’écran S’il y a des ifs, switch, loop….  Plus d’un && ou || If/else imbriqués Check NULL Des commentaires sont nécessaires pour expliquer la logique Une complexité élevée (cf sonar)
Des méthodes trop chargées Comment y remédier Découper en plusieurs autres méthodes Extraire d’autres classes et déléguer Favoriser le polymorphisme Retourner des objets vides plutôt que des NULL Donner des valeurs par défaut (pour éviter un else)
Au guichet du Grand Huit public   boolean  laisserPasser(Personne personne) { if  (personne.getAge() > 12 && personne.getTaille() > 1.3 && personne.estEnBonneSante()) { if  ( (personne.getAge() < 18 && personne.estAccompagne()) || (personne.getAge() >= 18)){ facturer(personne); return   true ; } } return   false ; } estPhysiquementCompatibleJeuxIntenses(personne) estLegalementCompatibleJeuxIntenses(personne)
Extraction de méthodes private   boolean  estLegalementCompatibleJeuxIntenses(Personne personne) { return  estMineurAccompagne(personne) || estMajeur(personne); } private   boolean  estPhysiquementCompatibleJeuxIntenses(Personne personne) { return  personne.getAge() > 12 && personne.getTaille() > 1.3 && personne.estEnBonneSante(); } private   boolean  estMajeur(Personne personne) { return  personne.getAge() >= 18; } private   boolean  estMineurAccompagne(Personne personne) { return  personne.getAge() < 18 && personne.estAccompagne(); }
Extraction d’une autre classe public   class  GrandHuitRefactore { private   PersonneVerificateur  personneChecker; public   boolean  laisserPasser(Personne personne) { if  (personneChecker.physiqueMinimum(personne) && personneChecker.estConsidereMajeur(personne)) { facturer(personne); return   true ; } return   false ; }
Polymorphisme public   class  Commande { protected   static   final   double   TAUX_REDUIT  = 0.5; protected   static   final   double   TAUX_PLEIN  = 1; public   void  facturer(Client client) { if  (client.isEtudiant()) { calculerTotal( TAUX_REDUIT ); prelever(); }  else  { calculerTotal( TAUX_PLEIN ); prelever(); } } abstract CommandeEtudiant CommandeStandard
CommandeEtudiant public   void  facturer(Client client) { calculerTotal( TAUX_REDUIT ); prelever(); } CommandeStandard public   void  facturer(Client client) { calculerTotal( TAUX_PLEIN ); prelever(); }
Symptômes d’un code intestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service Interroger des collaborateurs Des classes hyperactives Des méthodes trop chargées Mélanger les objets valeurs et les objets services
Opération générerFacture entrée sortie Facilement instanciable Getter/Setter Avec un état Objet valeur Est Objet service Fait
Objet valeur / Objet métier Objet valeur  Facile à instancier Pas de services dans le constructeur Orienté état Probablement pas d’interface Pas de comportement externe Objet service  Toujours injecté, jamais instancié Souvent une interface Souvent créateur d’objet valeur Orienté service A mocker Client Joueur Expresso BanqueService CommandeValidator BaseDeDonnees
Mélanger les objets valeurs et les objets services Pourquoi c’est mal Devoir tout mocker Tests couteux Comment y remédier Externaliser des classes valeurs Faire communiquer les services par des objets valeurs
Service Valeur Service Valeur Service
Symptômes d’un code intestable Isolabilité Simplicité Classes hyperactives Méthodes chargées Interroger des  collaborateurs Etats globaux Annuaires Blocs statiques Instanciation directe Constructeur cher Mélanger service et valeur Héritage
Vers du code testable Isolabilité Simplicité Passer les objets utilisés Directement en paramètre Pas de longues  initialisations Injecter les  dépendances Injecter les  dépendances Injecter les  dépendances Injecter les  dépendances Donner des veines pour les mocks Limiter  dépendances directes Supprimer les singletons, static et annuaires Petites classes  1 scénario = 1 test Séparer les  responsabilités Composition plutôt  Qu’héritage 1 classe = 1 responsabilité Petites  méthodes Polymorphisme
Outils Singleton detector Testability explorer
Ressources Références Clean code  talks , by M.Hevery (Google) Guide « Writing testable code » , by J.Wolter, R.Ruffer, M.Hevery Et aussi…. Writing testable code , by Isa Goksu (ThoughWorks)  Top 10  things   that   make  code hard to test , by M.Hevery (Google) How to  make   your  code testable , by CodeUtopia Livres xUnit Test Patterns Growing object oriented software Working effectively with legacy code Coder proprement Refactoring

Comment écrire du code testable ?

  • 1.
    Comment écrire ducode testable Conférence Agile France 2010 Florence CHABANOIS
  • 2.
  • 3.
  • 4.
    Frein ? Directionassistée? Boite de vitesse ? Pneus défectueux ?
  • 5.
  • 6.
    Tests unitaires Testerune partie du produit Simuler un comportement différent de la production EasyMock, Mockito, JMock Pour pouvoir tester unitairement : Les composants doivent être séparables
  • 7.
    Isolation Pour permettrela séparation Externaliser les dépendances public Moteur() { reservoirHuile = new ReservoirHuilePlein(); } public MoteurOk(ReservoirHuile reservoirHuile) { this .reservoirHuile = reservoirHuile; } Bannir les dépendances cachées
  • 8.
    Une solution LeTest Driven Development Given… When… Then Implémentation Refactoring Mais.. 1. Il y a souvent du code déjà existant … sur lequel il faut poser des tests … dont le nouveau code dépend
  • 9.
    Le TDD nedonne pas l’immunité 2. Le code peut être testé et Classes et méthodes fourre-tout Les tests souffrent Performances (constructeur couteux) Compréhensibilité (tests = spécifications) Le développeur aussi Tests lents Maintenabilité
  • 10.
    Leitmotiv Deux lignesde conduite Isolabilité Simplicité
  • 11.
  • 12.
    Symptômes d’un codeintestable Isolabilité Simplicité Classes hyperactives Méthodes chargées Interroger des collaborateurs Etats globaux Annuaires Blocs statiques Instanciation directe Constructeur cher Mélanger service et valeur Héritage
  • 13.
  • 14.
  • 15.
    public class Dictionnaire { private Map<String, String> definitions = new HashMap<String, String>(); public Dictionnaire() throws IOException { File file = new File(&quot;francais.txt&quot;); BufferedReader reader = new BufferedReader( new FileReader(file)); String ligne; while ((ligne = reader.readLine()) != null ) { String[] tableauLigne = ligne.split(&quot;:&quot;); definitions.put(tableauLigne[0], tableauLigne[1]); } } public String getDefinition(String mot) { return definitions.get(mot); }
  • 16.
    Test (n.m.) Opérationdestinée à contrôler le bon fonctionnement d'un appareil ou la bonne exécution d'un programme dans son ensemble.
  • 17.
    Tester getDefinition() @Testpublic void testGetDefinition() throws IOException { Dictionnaire dico = new Dictionnaire(); String returnedDefinition = dico.getDefinition(&quot;test&quot;); assertThat (returnedDefinition, is ( equalTo (&quot;Opération destinée à etc.&quot;))); }
  • 18.
    public Dictionnaire() throws IOException { File file = new File(&quot;francais.txt&quot;); BufferedReader reader = new BufferedReader( new FileReader(file)); String ligne; while ((ligne = reader.readLine()) != null ) { String[] tableauLigne = ligne.split(&quot;:&quot;); definitions.put(tableauLigne[0], tableauLigne[1]); } } Test très lent Obligé d’avoir un fichier
  • 19.
    @Test public void testGetDefinition_WhenMotNonTrouve() throws IOException { Dictionnaire dico = new Dictionnaire(); (…) } @Test public void testGetDefinition_WhenMotNonValide() throws IOException { Dictionnaire dico = new Dictionnaire(); (…) }
  • 20.
    Symptômes d’un codeintestable Un constructeur cher
  • 21.
    Un constructeur tropcher Pourquoi c’est mal On ne peut PAS éviter d’instancier une classe pour la tester Enlève une veine ( seam ) Test pas isolé Test potentiellement couteux Difficile de simuler un autre comportement Voire plus, si c’est utilisé par d’autres tests
  • 22.
    Un constructeur tropcher Signes d’alertes If, switch, loop new d’objets Des appels statiques … En fait autre chose que des assignations d’attributs
  • 23.
    Un constructeur tropcher Comment y remédier Méthode init() à appeler après le constructeur Pas de test dessus Un constructeur spécial pour le test Déplacement du problème Extraire dans une autre méthode, qu’on surcharge
  • 24.
    Code : Extractionde la méthode public DictionnairePatche() throws IOException { initialize(); } protected void initialize() throws FileNotFoundException, IOException { File file = new File(&quot;francais.txt&quot;); BufferedReader reader = new BufferedReader( new FileReader(file)); String ligne; while ((ligne = reader.readLine()) != null ) { String[] tableauLigne = ligne.split(&quot;:&quot;); definitions.put(tableauLigne[0], tableauLigne[1]); } }
  • 25.
    Test : instanciationd’une sous classe static class DictionnairePatchForTest extends DictionnairePatche { @Override protected void initialize () throws FileNotFoundException, IOException { // nothing } } @Test public void testGetDefinition() throws IOException { Dictionnaire dico = new DictionnairePatchForTest(); String returnedDefinition = dico.getDefinition(&quot;test&quot;); assertThat (returnedDefinition, is ( equalTo (&quot;Opération destinée à contrôler le bon fonctionnement d'un appareil ou la bonne exécution d'un programme dans son ensemble.&quot;))); }
  • 26.
    Un constructeur tropcher Comment y remédier Méthode init() à appeler après le constructeur Pas de test dessus Un constructeur spécial pour le test Déplacement du problème Extraire dans une autre méthode, qu’on surcharge Pas de test dessus
  • 27.
    Un constructeur tropcher Signes d’alertes mis à jour If, switch, loop new d’objets Des appels static … . En fait autre chose que des assignations d’attributs Un constructeur spécial test Init() Du code spécial test : @VisibleForTesting
  • 28.
    Un constructeur tropcher Comment y remédier Méthode init() Un constructeur spécial pour le test Extraire dans une autre méthode, qu’on surcharge Comment y remédier mieux Faire des constructeurs relais uniquement Passer les collaborateurs prêts en paramètres au lieu de les créer Injection de dépendances Factories
  • 29.
    public class DictionnaireTestable { private Map<String, String> definitions = new HashMap<String, String>(); public DictionnaireTestable( Map<String, String> definitions ) throws IOException { this .definitions = definitions; } } Le constructeur ne coute plus cher Veine créée : Le code n’est plus « collé »
  • 30.
    public class DictionnaireFactory { public static Dictionnaire buildFromTextFile() throws IOException { Map<String, String> definitions = new HashMap<String, String>(); File file = new File(&quot;francais.txt&quot;); BufferedReader reader = new BufferedReader( new FileReader(file)); String ligne; while ((ligne = reader.readLine()) != null ) { String[] tableauLigne = ligne.split(&quot;:&quot;); definitions.put(tableauLigne[0], tableauLigne[1]); } return new DictionnaireTestable(definitions); } } Séparation des responsabilités
  • 31.
  • 32.
    Principe de responsabilitéunique Je cherche sur Internet de quelle matière première j’ai besoin pour en fabriquer J’appelle Air France pour réserver un billet d’avion et aller en chercher en Chine Je demande au service Bureautique de m’en installer un nouveau
  • 33.
    Principe de responsabilitéunique Et le service Bureautique ? Cherche sur Internet de quelle matière première il a besoin pour en fabriquer Appelle Air France pour réserver un billet d’avion et aller en chercher en Chine Le commande chez son fournisseur
  • 34.
    Principe de responsabilitéunique Etudes Fournisseur Bureautique
  • 35.
    Principe de responsabilitéunique Créer le graphe d’objets est une responsabilité à part entière public class Moteur { private ReservoirHuile reservoirHuile ; public Moteur() { reservoirHuile = new ReservoirHuilePlein(); } public void demarrer () { // (...) } public void signalerManqueHuile () { // (...) } } Création du graphe d’objets Logique métier
  • 36.
    Focus sur demarrer()public void demarrer() { Moteur moteur = new Moteur(); moteur.demarrer(); BoiteDeVitesse boiteVitesse = new BoiteDeVitesse(); boiteVitesse.passerLaPremiere(); Embrayage embrayage = new Embrayage(); embrayage.relacher(); Accelerateur accelerateur = new Accelerateur(); accelerateur.appuyer(); }
  • 37.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes
  • 38.
    Des instanciations directesPourquoi c’est mal Couplage fort Enlève une veine ( seam ) Test pas isolé Test potentiellement couteux Difficile de simuler un autre comportement
  • 39.
    Des instanciations directesSignes d’alertes Des « new » dans une classe autre que Factory ou Builder
  • 40.
    Des instanciations directesComment y remédier Framework de mocks : JMockit, Powermock Comment y remédier mieux Passer les objets nécessaires en paramètres de la méthode Séparer construction du graphe d’objets de la logique métier Injection de dépendances Factories
  • 41.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes Des blocs statiques
  • 42.
    Des blocs statiquespublic class Joueur { private static Plateau plateau ; static { if (Environnement. IS_DEMO ) { plateau = new PlateauCommercial(); } else { plateau = new PlateauDeDemo(); } } public void joindre(Partie partie) { } }
  • 43.
    Des blocs statiquesPourquoi c’est mal Couplage très fort Pas possible de le remplacer par un mock Ni de le surcharger dans les tests Potentiellement très couteux Effets de bord entre des tests censés être isolés Le test passe, parfois Etat permanent
  • 44.
    Des blocs statiquesSignes d’alertes Static {} Un test qui ne fonctionne plus au sein d’une suite Comment y remédier Supprimer tous les bloc statiques et introduire des classes Passer les collaborateurs en paramètres au lieu de les créer Injection de dépendances Factories
  • 45.
    public class JoueurTestable { private Plateau plateau; public JoueurTestable( Plateau plateau ) { this .plateau = plateau; } public void joindre(Partie partie) { (…) } }
  • 46.
    Spring-jeu.xml <bean class=&quot;fr.soat.agileconference2010.blocstatic.JoueurTestable&quot; id= &quot;joueur1&quot; scope= &quot;prototype&quot; > <constructor-arg ref= &quot;plateau&quot; ></constructor-arg> </bean> <bean class= &quot;fr.soat.agileconference2010.blocstatic.metier. PlateauCommercial &quot; id= &quot;plateau&quot; scope= &quot; singleton &quot; ></bean>
  • 47.
    testJoindre() Plateau plateau= new PlateauDeDemo (); JoueurTestable joueur = new JoueurTestable(plateau); joueur.joindre( new Partie()); //Verifications
  • 48.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes
  • 49.
  • 50.
  • 51.
    Des dynasties declasses Pourquoi c’est mal Couplage fort avec classe mère Lenteur Fragilité Tests plus difficiles à maintenir (redondance)
  • 52.
    Des dynasties declasses Signes d’alertes Quand le code devient difficile à tester Quand les tests sont redondants / difficile à maintenir à cause de la classe mère Comment y remédier Utiliser la composition pour réutiliser du code Limiter l’héritage aux besoins de polymorphisme
  • 53.
  • 54.
  • 55.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux
  • 56.
    a = newX().traiter(); b = new X().traiter(); a = b ?
  • 57.
    Source : http://misko.hevery.com/2009/10/07/design-for-testability-talk/
  • 58.
  • 59.
    On veut poserun test sur l’expresso public class MachineACafe { public void payer( float montant){ (…) } public Expresso preparerExpresso() { (…) } public void brancher(){ (…) } }
  • 60.
    Test de l’expresso @Test public void testPreparerExpresso() { MachineACafe machineACafe = new MachineACafe(); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(), is ( true )); } Null Pointer Exception
  • 61.
    Test de l’expresso@Test public void testPreparerExpressoEssai2() { MachineACafe machineACafe = new MachineACafe(); machineACafe.setBaseDeDonnees( new BaseDeDonnees()); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(), is ( true )); } Null Pointer Exception
  • 62.
    Test de l’expresso@Test public void testPreparerExpressoEssai3() { MachineACafe machineACafe = new MachineACafe(); BaseDeDonnees baseDeDonnees = new BaseDeDonnees(); baseDeDonnees.init(&quot;myUrl&quot;, &quot;myLogin&quot;, &quot;myPassword&quot;); machineACafe.setBaseDeDonnees(baseDeDonnees); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(), is ( true )); } Null Pointer Exception
  • 63.
    Test de l’expresso@Test public void testPreparerExpressoEssai4() { MachineACafe machineACafe = new MachineACafe(); BaseDeDonnees baseDeDonnees = new BaseDeDonnees(); baseDeDonnees.init(&quot;myUrl&quot;, &quot;myLogin&quot;, &quot;myPassword&quot;); baseDeDonnees.setNotificateur( new Notificateur()); machineACafe.setBaseDeDonnees(baseDeDonnees); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(), is ( true )); } CafeException
  • 64.
    Pourquoi CafeException ?public void verifierPreconditions() { if (! robinetActive ()) { final String erreur = &quot;Vérifier le robinet&quot;; baseDeDonnees.logguerErreur( this , erreur); throw new CafeException(erreur); } Hein, quel robinet ?
  • 65.
    Pourquoi CafeException ?private boolean robinetActive() { Robinet robinet = Robinet. getInstance (); return ( robinet.estOuvert () && robinet.estConnecte( this )); }
  • 66.
    Test de l’expresso@Test public void testPreparerExpressoEssai5() { MachineACafe machineACafe = new MachineACafe(); final BaseDeDonnees baseDeDonnees = new BaseDeDonnees(); baseDeDonnees.init(&quot;myUrl&quot;, &quot;myLogin&quot;, &quot;myPassword&quot;); baseDeDonnees.setNotificateur( new Notificateur()); machineACafe.setBaseDeDonnees(baseDeDonnees); Robinet. getInstance ().ouvrir(); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(), is ( true )); } Ok ! Ok !
  • 67.
    Je dev Jedev Je dev Je dev Je dev Je dev Je dev Je dev Mon code Il dev Il dev Il dev COMMIT Il dev Il dev Il dev Il dev Il dev COMMIT Son code
  • 68.
    Boom public void testPreparerVerreEau_whenDefaultValues() { FontaineAEau fontaine = new FontaineAEau(); VerreEau verre = fontaine.preparerVerreEau(); assertThat (verre, is ( nullValue ())); }} AssertionError : expected NULL « Son code »
  • 69.
    Test de l’expresso@Test public void testPreparerExpressoEssai6() { MachineACafe machineACafe = new MachineACafe(); final BaseDeDonnees baseDeDonnees = new BaseDeDonnees(); baseDeDonnees.init(&quot;myUrl&quot;, &quot;myLogin&quot;, &quot;myPassword&quot;); baseDeDonnees.setNotificateur( new Notificateur()); machineACafe.setBaseDeDonnees(baseDeDonnees); Robinet. getInstance ().ouvrir(); Expresso expresso = machineACafe.preparerExpresso(); assertThat (expresso.estConforme(), is ( true )); Robinet. getInstance ().fermer(); } Ok pour le moment…
  • 70.
    Des états globauxPourquoi c’est mal Mensonge : « il n’y a pas de dépendances. » Méthode statique ou Singleton = dépendance cachée. Pas de veine pour placer un mock Test pas isolé Test potentiellement couteux Difficile de simuler un autre comportement Risque de perturbations avec d’autres tests Etat présumé Plus longs à lancer Débogage difficile
  • 71.
    Des états globauxSignes d’alertes Des singletons Du code static : variable, bloc, méthode … même un seul !!! « chargement global (global load) » : nombre de variables pouvant être modifiées par un état global Des tests qui fonctionnent seuls mais pas en groupe ou vice versa
  • 72.
    Des états globauxComment y remédier Suppression du final et introduction de setters Isoler le problème dans une autre méthode, qu’on surcharge. Violation de l’encapsulation Code brouillé Et peu nettoyable Oubli de reset Ordre compte Lisibilité
  • 73.
    Des états globauxSignes d’alertes mis à jour Des singletons Du code static : variable, bloc, méthode … même un seul !!! car « chargement global / global load » : le nombre de variables qui peuvent être modifiées par un état global Des tests qui fonctionnent seuls mais pas en groupe ou vice versa Du code spécial test Des setters, reset, init dans les singletons @VisibleForTesting
  • 74.
    Des états globauxComment y remédier réellement Bannir singleton et code static Décliner en classes Injection de dépendances
  • 75.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service
  • 76.
    Annuaire de servicespublic Maison(Locator locator) { porte = locator.getPorte(); fenetre = locator.getFenetre(); toit = locator.getToit(); }
  • 77.
    Annuaire de servicesPourquoi c’est mal Tromperie « il n’y a pas de dépendances » « il n’y en a qu’une seule » Application entière à initialiser
  • 78.
    Annuaire de servicesSignes d’alertes « Registry », « context », « locator » Comment y remédier Passer les objets réellement utilisés Injection de dépendances
  • 79.
  • 80.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service Interroger des collaborateurs
  • 81.
  • 82.
    Avoir des intermédiairespublic void facturer(Commande commande, Client client) { banqueService.prelever(client.getCompteBancaire(), commande.getTotal() ); emailService.notifierPrelevement(client.getEmail()); }
  • 83.
    Avoir des intermédiairesPourquoi c’est mal Tromperie : « on a besoin de Commande et Client » Couplage fort avec l’objet intermédiaire Lisibilité Débogage plus complexe (exception) Initialisation du test plus complexe
  • 84.
    Avoir des intermédiairesSignes d’alertes « context », « environment », « container » Objets passés mais jamais utilisés directement Plus d’un point env.getUser().autoconnect(); Dans les tests : Des mocks qui retournent des mocks Devoir mocker des getters/setters
  • 85.
    Avoir des intermédiairesComment y remédier Appliquer le principe de connaissance minimale (Loi de Demeter) toute méthode M d'un objet O peut uniquement invoquer les méthodes de lui-même ses attributs ses paramètres les objets qu'il crée/instancie Passer directement les objets réellement utilisés
  • 86.
    public void facturer(CompteBancaire compte , double montant , String email ) { banqueService.prelever(compte, montant); emailService.notifierPrelevement(email); }
  • 87.
    Initialisation du testavant Client client = new Client(); final CompteBancaire compte = new CompteBancaire(); client.setCompteBancaire(compte); final String email = &quot;toto@email.fr&quot;; client.setEmail(email); Commande commande = new Commande(); final double total = 20.0; commande.setTotal(total); // When manager.facturer(commande, client);
  • 88.
    Initialisation du testaprès final CompteBancaire compte = new CompteBancaire(); final String email = &quot;toto@email.fr&quot;; final double montant = 20.0; // When manager.facturer(compte, montant, email);
  • 89.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service Interroger des collaborateurs Des classes hyperactives
  • 90.
  • 91.
    Des classes hyperactivesPourquoi c’est mal Classe fourre-tout Peu robuste aux changements Lisibilité Maintenabilité
  • 92.
    Des classes hyperactivesSignes d’alertes « manager », « utils », « helper » Qu’est ce qu’elle fait? Et Pas évidente à comprendre pour un nouvel arrivant / Pas facile d’avoir en tête ce qu’elle fait en une fois Difficile de trouver un nom à la classe Quand un champ n’est utilisé que par quelques méthodes Beaucoup de champs et/ou collaborateurs Beaucoup de méthodes Méthodes avec peu de rapport les unes les autres Méthodes statiques
  • 93.
    Des classes hyperactivesComment y remédier Etapes Identifier les responsabilités de la classe Les nommer Les extraire dans autant de classes Une classe peut avoir le rôle d’orchestrer Comment identifier les responsabilités? Repérer les méthodes qui ne sont utilisées que par un ou quelques champs Repérer les méthodes statiques et les rendre à leur paramètres (ou wrapper de paramètres) listerCommandes(Client client) Regrouper méthodes qui se ressemblent Regrouper les attributs souvent utilisés ensemble
  • 94.
    Des classes hyperactivesComment y remédier (suite) Si code legacy Extraire une classe pour chaque modification / nouvelle fonctionnalité
  • 95.
    Des classes hyperactivesComment y remédier (suite) Si code legacy Extraire une classe pour chaque modification / nouvelle fonctionnalité Imbriquer les collaborateurs A Y Z X W A Y Z X W
  • 96.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service Interroger des collaborateurs Des classes hyperactives Des méthodes trop chargées
  • 97.
    Au guichet duGrand Huit public boolean laisserPasser(Personne personne) { if (personne.getAge() > 12 && personne.getTaille() > 1.3 && personne.estEnBonneSante()) { if ( (personne.getAge() < 18 && personne.estAccompagne()) || (personne.getAge() >= 18)){ facturer(personne); return true ; } } return false ; }
  • 98.
    Des méthodes tropchargées Pourquoi c’est mal Augmente la complexité des tests Très sensible aux modifications Difficile de comprendre tout de suite le fonctionnement
  • 99.
    Des méthodes tropchargées Signes d’alertes Si ça dépasse l’écran S’il y a des ifs, switch, loop…. Plus d’un && ou || If/else imbriqués Check NULL Des commentaires sont nécessaires pour expliquer la logique Une complexité élevée (cf sonar)
  • 100.
    Des méthodes tropchargées Comment y remédier Découper en plusieurs autres méthodes Extraire d’autres classes et déléguer Favoriser le polymorphisme Retourner des objets vides plutôt que des NULL Donner des valeurs par défaut (pour éviter un else)
  • 101.
    Au guichet duGrand Huit public boolean laisserPasser(Personne personne) { if (personne.getAge() > 12 && personne.getTaille() > 1.3 && personne.estEnBonneSante()) { if ( (personne.getAge() < 18 && personne.estAccompagne()) || (personne.getAge() >= 18)){ facturer(personne); return true ; } } return false ; } estPhysiquementCompatibleJeuxIntenses(personne) estLegalementCompatibleJeuxIntenses(personne)
  • 102.
    Extraction de méthodesprivate boolean estLegalementCompatibleJeuxIntenses(Personne personne) { return estMineurAccompagne(personne) || estMajeur(personne); } private boolean estPhysiquementCompatibleJeuxIntenses(Personne personne) { return personne.getAge() > 12 && personne.getTaille() > 1.3 && personne.estEnBonneSante(); } private boolean estMajeur(Personne personne) { return personne.getAge() >= 18; } private boolean estMineurAccompagne(Personne personne) { return personne.getAge() < 18 && personne.estAccompagne(); }
  • 103.
    Extraction d’une autreclasse public class GrandHuitRefactore { private PersonneVerificateur personneChecker; public boolean laisserPasser(Personne personne) { if (personneChecker.physiqueMinimum(personne) && personneChecker.estConsidereMajeur(personne)) { facturer(personne); return true ; } return false ; }
  • 104.
    Polymorphisme public class Commande { protected static final double TAUX_REDUIT = 0.5; protected static final double TAUX_PLEIN = 1; public void facturer(Client client) { if (client.isEtudiant()) { calculerTotal( TAUX_REDUIT ); prelever(); } else { calculerTotal( TAUX_PLEIN ); prelever(); } } abstract CommandeEtudiant CommandeStandard
  • 105.
    CommandeEtudiant public void facturer(Client client) { calculerTotal( TAUX_REDUIT ); prelever(); } CommandeStandard public void facturer(Client client) { calculerTotal( TAUX_PLEIN ); prelever(); }
  • 106.
    Symptômes d’un codeintestable Un constructeur cher Des instanciations directes Des blocs statiques Une dynastie de classes Des états globaux Annuaire de service Interroger des collaborateurs Des classes hyperactives Des méthodes trop chargées Mélanger les objets valeurs et les objets services
  • 107.
    Opération générerFacture entréesortie Facilement instanciable Getter/Setter Avec un état Objet valeur Est Objet service Fait
  • 108.
    Objet valeur /Objet métier Objet valeur Facile à instancier Pas de services dans le constructeur Orienté état Probablement pas d’interface Pas de comportement externe Objet service Toujours injecté, jamais instancié Souvent une interface Souvent créateur d’objet valeur Orienté service A mocker Client Joueur Expresso BanqueService CommandeValidator BaseDeDonnees
  • 109.
    Mélanger les objetsvaleurs et les objets services Pourquoi c’est mal Devoir tout mocker Tests couteux Comment y remédier Externaliser des classes valeurs Faire communiquer les services par des objets valeurs
  • 110.
    Service Valeur ServiceValeur Service
  • 111.
    Symptômes d’un codeintestable Isolabilité Simplicité Classes hyperactives Méthodes chargées Interroger des collaborateurs Etats globaux Annuaires Blocs statiques Instanciation directe Constructeur cher Mélanger service et valeur Héritage
  • 112.
    Vers du codetestable Isolabilité Simplicité Passer les objets utilisés Directement en paramètre Pas de longues initialisations Injecter les dépendances Injecter les dépendances Injecter les dépendances Injecter les dépendances Donner des veines pour les mocks Limiter dépendances directes Supprimer les singletons, static et annuaires Petites classes 1 scénario = 1 test Séparer les responsabilités Composition plutôt Qu’héritage 1 classe = 1 responsabilité Petites méthodes Polymorphisme
  • 113.
    Outils Singleton detectorTestability explorer
  • 114.
    Ressources Références Cleancode talks , by M.Hevery (Google) Guide « Writing testable code » , by J.Wolter, R.Ruffer, M.Hevery Et aussi…. Writing testable code , by Isa Goksu (ThoughWorks) Top 10 things that make code hard to test , by M.Hevery (Google) How to make your code testable , by CodeUtopia Livres xUnit Test Patterns Growing object oriented software Working effectively with legacy code Coder proprement Refactoring

Notes de l'éditeur

  • #2 On va commencer par un petit sondage En fait je veux dire unitaire Qui fait systématiquement des tests? Qui essaie toujours? Quels problemes? Des tests qui marchent tout seul et pas en groupe, au moment de la release. Des NPE à ajouter partout;
  • #3 http://www.internetkids.fr/public/plus-belle-plage.jpg
  • #4 http://www.thau-agglo.fr/IMG/jpg/13_Une_partie_de_Beach_Volley_x700_.jpg
  • #5 http://bichett01.skyrock.com/photo.html?id_article=1242496956&amp;rev=0
  • #6 http://img.turbo.fr/02665474-photo-pieces-detachees-accessoires.jpg .. un pb et plusieurs causes possibles .. Unité avec branchement pour permettre tester isolément .. Et des cas d’erreurs (introduction de fakes)  voyants qui doivent apparaitre
  • #7 Qu’est ce qu’on y gagne? Satisfaction : on sait que ça marche Qd pb, on peut innocenter le code Specs Rassure, confiance pour remanier le code Il faut des branchements
  • #8 On ne veut pas d’effets de bord. Savoir d’où vient un pb. … donner des veines / des branchements l’intérieur d’un PC … exemple d’une boite noir : - un truc soudé - une freebox
  • #9 Given : carburant + huile moteur + batterie ok When : tourne la clef Then : démarrage du moteur
  • #11 Ok en théorie mais concrètement, comment reconnaître le loup? Savoir, mais c pas si evident de le reconnaître. L’idée c’est aussi de savoir reconnaître les signes d’un code intestable. Ce qui fait qu’on sait qu’un loup et un loup, meme s’il est déguisé en mere grand.
  • #15 Pour le premier symptome
  • #18 IO + boucle Lenteur à trimballer pour tous les cas de test
  • #31 Graphe d’objets / logique métier Dico : obtenir les définitions Factory : construire la référence des définitions
  • #34 Développer des logiciels Installer des logiciels et du matériel Fournir du matériel
  • #35 Développer des logiciels Installer des logiciels et du matériel Fournir du matériel
  • #36 Mélane logique métier + création du graphe d’objets
  • #37 Du coup quand on voit que DANS demarrer, on construit des objets, qq chose cloche
  • #40 Objet valeur ne coute pas à etre instancier.
  • #41 Mais dépendant du framework Un peu bourrin : toutes les instances sont mockées Peu répandu
  • #42 Le meme genre de probleme
  • #44 Impossible de simuler un autre comportement Impossible de changer son état une fois que c’est initialisé =&gt; Certains tests peuvent marcher
  • #49 Le meme genre de probleme
  • #50 L’héritage est une bonne idée mais il est souvent mal utilisé Un lion court comment? … à quatre pattes Souris aussi Donc j’en hérite – un peu extrême car ce n’est pas un « est »
  • #51 Toutes les sous classes sont fortement couplées aux implémentations de leur classe mère Des fois ça ne se justifie pas Des fois oui, mais ca rend les choses trop compliquées à tester. Est-ce que ça vaut le coup? seDeplacer manger
  • #52 - fragile aux modifications de la classe mere - potentiellement lent - pas la main sur le comportement hérité : on peut pas lui en simuler un autre
  • #56 - 5e symptome qui empeche le code d&apos;etre isolable
  • #58 Les etats globaux sont un véritable fléau, alors pour eux, on va prendre un exemple un peu plus long.
  • #59 - tiers de la prez, c&apos;est l&apos;heure de prendre un café - &amp;quot;je vous propose de prendre un café&amp;quot;
  • #64 BDD pas completement initialisé
  • #66 Robinet est fermé par défaut.
  • #67 Ouf… mais il a quand meme fallu tatonné pour pouvoir tout initialisé. Autant de dépendances cachées.
  • #71 Débogage difficile Si oubli de réinitialiser l’état à la fin Pas de tests en parallèle Chaque test doit commencer avec un état présumé pour fonctionner. Mais cela peut avoir été modifié par un autre test. Impossible de lancer les tests en parallèle =&gt; + long Débogage difficile
  • #72 Un seul singleton = 100 valeurs partagées
  • #73 Mais modification du code de production pour le test… très intrusif. Violation de l’encapsulation. Et plus tard on croit que c’est pour la prod =&gt; jamais nettoyé. Meme si on spécifie que c’est pour les tests, on peut oublier de resetter l’état à la fin et perturber un autre test. Non unitaire Très difficile à déboguer Les tests s’impactent les uns les autres
  • #75 Static = classes manquantes. Identifier les différentes responsabilités et en extraire des classes.
  • #76 Le meme genre de probleme
  • #78 nous ne savons pas ce qu&apos;il faut réellement mocker avant d&apos;avoir lancé le code, bien qu&apos;on puisse le soupçonner en regardant les attributs de la classe ; il est difficile de connaître les dépendances cachées de la classe ; si l&apos;on ajoute une dépendance à la classe, cela ne se verra pas clairement à cause du Locator. Le code compilera toujours, certains tests passeront, d&apos;autres pas ; pour avoir une House, nous avons besoin de manipuler un Locator dans le test. Cependant, ce dernier a aussi connaissance d&apos;autres services, qu&apos;il faut aussi créer. De fil en aiguille, c&apos;est une application entière qu&apos;il faut construire pour les tests... il faut rendre le Locator compatible avec nos tests en créant une interface spécifique, en le surchargeant, en le mockant ou en rajoutant des setters pour pouvoir y injecter nos dépendances ; ils mélangent la construction des objets avec les lookup d&apos;objets.
  • #80 - on aborde maintenant des symptomes, qui sans etre intestables sont genantes.
  • #81 Le meme genre de probleme
  • #82 Est-ce qu’il vaut mieux acheter au producteur ou à un intermédiaire? Pourquoi? Plus direct Moins cher
  • #83 Violation du SRP (collaborateur qui sert de service locator)  Meme mensonge
  • #84 Violation du SRP (collaborateur qui sert de service locator) Pourquoi c’est mal Tromperie : on a besoin « Utilisateur et Commande » Il faut regarder dans le code Débogage complexifié : D’où vient telle exception? Gâchis et moins de lisibilité En se trainant cet objet qu’on n’utilise pas Couplage fort avec l’objet intermédiaire Difficile à enlever plus tard Une modification peut avoir beaucoup d’impacts Du côté des tests : Laborieux : on passe le context et corrige les NPE découvert au fur et à mesure Phase d’initialisation du test complexifiée Test fortement couplé à l’implémentation avec l’intermédiaire
  • #85 Fausse bonne idée car Induit en erreur car on croit réellement qu’on a juste besoin de « context ». Pour écrire le test, il faut corriger les NPE au fur et à mesure, à l’aveugle. Pas si flexible qu’on croit car on peut pas refactorer facilement. On ne sait pas quels sont les collab nécessaires juste en regardant l’API. On ne comprend pas les enjeux du code facilement du coup.
  • #86 Fausse bonne idée car Induit en erreur car on croit réellement qu’on a juste besoin de « context ». Pour écrire le test, il faut corriger les NPE au fur et à mesure, à l’aveugle. Pas si flexible qu’on croit car on peut pas refactorer facilement. On ne sait pas quels sont les collab nécessaires juste en regardant l’API. On ne comprend pas les enjeux du code facilement du coup.
  • #90 Le meme genre de probleme
  • #92 - lisibilité de la classe (ca revient à avoir un grand main et tout dedans, une appli en C) - enlever &amp;quot;test plus complexe&amp;quot; et remonter &amp;quot;lisibilité&amp;quot; et &amp;quot;maintenabilité&amp;quot; Pour chaque modif, ce sera elle à modifier Debogage : si beaucoup d
  • #93 Très très fréquent
  • #94 Méthode statique : méthode à qui il manque une classe
  • #95 Méthode statique : méthode à qui il manque une classe
  • #96 Méthode statique : méthode à qui il manque une classe
  • #98 - si condition 1 rempli, si 1 et 2, si 2 et 3, si 1 et 3 =&gt; false. Tests peu robuste aux changements.
  • #100 S’il y a plusieurs chemins d’exécutions possibles et que c’est confus Design pattern stratégies Vérification d’objets NULL
  • #101 Design pattern stratégies Surtout si if redondants
  • #102 Pour chaque des conditions, on verifie que facturer et appeler et que true est retourné
  • #107 Le meme genre de probleme
  • #108 GenererFacture Pour vérifier qu’un service marche
  • #109 Ont des données Font des choses
  • #112 Savoir, mais c pas si evident de le reconnaître. L’idée c’est aussi de savoir reconnaître les signes d’un code intestable. Ce qui fait qu’on sait qu’un loup et un loup, meme s’il est déguisé en mere grand.
  • #113 Exceptions Si l’objet global est une constante / immutable Et ses attributs transitifs aussi Ok si primitif, attention si objet (ça peut changer) Si l’information ne va que dans un sens Logger
  • #115 Pour les méthodes et classes longues