View
4.669
Download
2
Category
Preview:
Citation preview
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éparationExternaliser les dépendancespublic 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é etClasses et méthodes fourre-toutLes tests souffrent• Performances (constructeur couteux)• Compréhensibilité (tests = spécifications)
Le développeur aussi• Tests lents• Maintenabilité
Symptômes d’un code intestable
Isolabilité Simplicité
Classes hyperactives
Méthodes chargées
Interroger des collaborateurs
Etats globauxAnnuaires
Blocs statiques
Instanciationdirecte
Constructeur cher
Mélangerservice et valeur
Héritage
public class Dictionnaire { private Map<String, String> definitions = new HashMap<String,
String>();
public Dictionnaire() throws IOException { File file = new File("francais.txt"); BufferedReader reader = new BufferedReader(new
FileReader(file)); String ligne; while ((ligne = reader.readLine()) != null) { String[] tableauLigne = ligne.split(":"); 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("test"); assertThat(returnedDefinition, is(equalTo("Opération
destinée à etc."))); }
public Dictionnaire() throws IOException { File file = new File("francais.txt"); BufferedReader reader = new BufferedReader(new
FileReader(file)); String ligne; while ((ligne = reader.readLine()) != null) { String[] tableauLigne = ligne.split(":"); definitions.put(tableauLigne[0], tableauLigne[1]); } }
Test très lentObligé 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();(…)
}
Un constructeur trop cher
Pourquoi c’est mal On ne peut PAS éviter d’instancier une classe pour la testerEnlève une veine (seam)
Test pas isoléTest potentiellement couteuxDifficile de simuler un autre comportement
Voire plus, si c’est utilisé par d’autres tests
Un constructeur trop cher
Signes d’alertesIf, switch, loopnew d’objets Des appels statiques
… En fait autre chose que des assignations d’attributs
Un constructeur trop cher
Comment y remédierMéthode init() à appeler après le constructeur
Pas de test dessus
Un constructeur spécial pour le testDé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("francais.txt"); BufferedReader reader = new BufferedReader(new
FileReader(file)); String ligne; while ((ligne = reader.readLine()) != null) { String[] tableauLigne = ligne.split(":"); 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("test"); assertThat(returnedDefinition, is(equalTo("Opération destinée à
contrôler le bon fonctionnement d'un appareil ou la bonne exécution d'un programme dans son ensemble.")));
}
Un constructeur trop cher
Comment y remédierMéthode init() à appeler après le constructeur
Pas de test dessus
Un constructeur spécial pour le testDé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 à jourIf, switch, loopnew d’objets Des appels static…. En fait autre chose que des assignations d’attributsUn constructeur spécial testInit() Du code spécial test : @VisibleForTesting
Un constructeur trop cher
Comment y remédierMéthode init() Un constructeur spécial pour le testExtraire dans une autre méthode, qu’on surcharge
Comment y remédier mieuxFaire des constructeurs relais uniquementPasser les collaborateurs prêts en paramètres au lieu de les créer
Injection de dépendancesFactories
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("francais.txt"); BufferedReader reader = new BufferedReader(new
FileReader(file)); String ligne; while ((ligne = reader.readLine()) != null) { String[] tableauLigne = ligne.split(":"); definitions.put(tableauLigne[0], tableauLigne[1]); } return new DictionnaireTestable(definitions); }} Séparation des
responsabilités
Principe de responsabilité unique
1. Je cherche sur Internet de quelle matière première j’ai besoin pour en fabriquer
2. J’appelle Air France pour réserver un billet d’avion et aller en chercher en Chine
3. Je demande au service Bureautique de m’en installer un nouveau
Principe de responsabilité unique
Et le service Bureautique ?1. Cherche sur Internet de quelle matière
première il a besoin pour en fabriquer2. Appelle Air France pour réserver un billet
d’avion et aller en chercher en Chine3. Le commande chez son fournisseur
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(); }
Des instanciations directes
Pourquoi c’est malCouplage fortEnlève une veine (seam)
Test pas isoléTest potentiellement couteuxDifficile de simuler un autre comportement
Des instanciations directes
Signes d’alertesDes « new » dans une classe autre que Factory ou Builder
Des instanciations directes
Comment y remédierFramework de mocks : JMockit, Powermock
Comment y remédier mieuxPasser les objets nécessaires en paramètres de la méthodeSéparer construction du graphe d’objets de la logique métier
Injection de dépendancesFactories
Symptômes d’un code intestable
1. Un constructeur cher2. Des instanciations directes3. 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 malCouplage très fort
Pas possible de le remplacer par un mockNi de le surcharger dans les testsPotentiellement très couteux
Effets de bord entre des tests censés être isolésLe test passe, parfoisEtat permanent
Des blocs statiques
Signes d’alertesStatic {}Un test qui ne fonctionne plus au sein d’une suite
Comment y remédierSupprimer tous les bloc statiques et introduire des classesPasser les collaborateurs en paramètres au lieu de les créer
Injection de dépendancesFactories
public class JoueurTestable { private Plateau plateau; public JoueurTestable(Plateau plateau) { this.plateau = plateau; } public void joindre(Partie partie) { (…) }}
Spring-jeu.xml
<bean class="fr.soat.agileconference2010.blocstatic.JoueurTestable" id="joueur1" scope="prototype">
<constructor-arg ref="plateau"></constructor-arg></bean>
<bean class="fr.soat.agileconference2010.blocstatic.metier.PlateauCommercial" id="plateau" scope="singleton"></bean>
testJoindre()
Plateau plateau = new PlateauDeDemo(); JoueurTestable joueur = new
JoueurTestable(plateau); joueur.joindre(new Partie()); //Verifications
Symptômes d’un code intestable
1. Un constructeur cher2. Des instanciations directes3. Des blocs statiques4. Une dynastie de classes
Des dynasties de classes
Pourquoi c’est malCouplage fort avec classe mèreLenteurFragilitéTests plus difficiles à maintenir (redondance)
Des dynasties de classes
Signes d’alertesQuand le code devient difficile à tester Quand les tests sont redondants / difficile à maintenir à cause de la classe mère
Comment y remédierUtiliser la composition pour réutiliser du codeLimiter l’héritage aux besoins de polymorphisme
Symptômes d’un code intestable
1. Un constructeur cher2. Des instanciations directes3. Des blocs statiques4. Une dynastie de classes5. Des états globaux
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("myUrl", "myLogin", "myPassword");
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("myUrl", "myLogin", "myPassword");
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 = "Vérifier le robinet"; 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("myUrl", "myLogin",
"myPassword"); baseDeDonnees.setNotificateur(new Notificateur()); machineACafe.setBaseDeDonnees(baseDeDonnees); Robinet.getInstance().ouvrir(); Expresso expresso = machineACafe.preparerExpresso(); assertThat(expresso.estConforme(), is(true)); }
Ok !Ok !
Je devJe dev
Je devJe dev
Je devJe dev
Je devJe dev
Mon code
Il devIl dev
Il dev COMMITIl dev
Il devIl dev
Il devIl 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("myUrl", "myLogin", "myPassword"); baseDeDonnees.setNotificateur(new Notificateur()); machineACafe.setBaseDeDonnees(baseDeDonnees); Robinet.getInstance().ouvrir(); Expresso expresso = machineACafe.preparerExpresso(); assertThat(expresso.estConforme(), is(true)); Robinet.getInstance().fermer(); }
Okpour le moment…
Des états globaux
Pourquoi c’est malMensonge : « il n’y a pas de dépendances. »
Méthode statique ou Singleton = dépendance cachée.
Pas de veine pour placer un mockTest pas isoléTest potentiellement couteuxDifficile de simuler un autre comportement
Risque de perturbations avec d’autres testsEtat présumé Plus longs à lancerDébogage difficile
Des états globaux
Signes d’alertesDes singletonsDu 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édierSuppression 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 à jourDes singletonsDu 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 groupeou vice versa
Du code spécial testDes setters, reset, init dans les singletons@VisibleForTesting
Des états globaux
Comment y remédier réellementBannir singleton et code staticDécliner en classesInjection de dépendances
Symptômes d’un code intestable
1. Un constructeur cher2. Des instanciations directes3. Des blocs statiques4. Une dynastie de classes5. Des états globaux6. 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 malTromperie
« 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édierPasser les objets réellement utilisésInjection de dépendances
Symptômes d’un code intestable
1. Un constructeur cher2. Des instanciations directes3. Des blocs statiques4. Une dynastie de classes5. Des états globaux6. Annuaire de service7. 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 malTromperie : « on a besoin de Commande et Client »
Couplage fort avec l’objet intermédiaireLisibilité 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 mocksDevoir mocker des getters/setters
Avoir des intermédiaires
Comment y remédierAppliquer 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 = "toto@email.fr"; client.setEmail(email);
Commande commande = new Commande(); final double total = 20.0; commande.setTotal(total);
// Whenmanager.facturer(commande, client);
Initialisation du test après
final CompteBancaire compte = new CompteBancaire();
final String email = "toto@email.fr"; final double montant = 20.0;
// Whenmanager.facturer(compte, montant, email);
Symptômes d’un code intestable
1. Un constructeur cher2. Des instanciations directes3. Des blocs statiques4. Une dynastie de classes5. Des états globaux6. Annuaire de service7. Interroger des collaborateurs8. Des classes hyperactives
Des classes hyperactives
Pourquoi c’est malClasse fourre-toutPeu robuste aux changementsLisibilitéMaintenabilité
Des classes hyperactives
Signes d’alertes« manager », « utils », « helper »Qu’est ce qu’elle fait? EtPas évidente à comprendre pour un nouvel arrivant / Pas facile d’avoir en tête ce qu’elle fait en une foisDifficile de trouver un nom à la classeQuand un champ n’est utilisé que par quelques méthodesBeaucoup de champs et/ou collaborateursBeaucoup de méthodesMéthodes avec peu de rapport les unes les autresMéthodes statiques
Des classes hyperactives
Comment y remédierEtapes1. Identifier les responsabilités de la classe2. Les nommer3. Les extraire dans autant de classes4. Une classe peut avoir le rôle d’orchestrerComment identifier les responsabilités?
Repérer les méthodes qui ne sont utilisées que par un ou quelques champsRepérer les méthodes statiques et les rendre à leur paramètres (ou wrapper de paramètres)• listerCommandes(Client client)
Regrouper méthodes qui se ressemblentRegrouper 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
YZX
W A
Y ZX
W
Symptômes d’un code intestable
1. Un constructeur cher2. Des instanciations directes3. Des blocs statiques4. Une dynastie de classes5. Des états globaux6. Annuaire de service7. Interroger des collaborateurs8. Des classes hyperactives9. 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 malAugmente la complexité des testsTrès sensible aux modificationsDifficile de comprendre tout de suite le fonctionnement
Des méthodes trop chargées
Signes d’alertesSi ça dépasse l’écranS’il y a des ifs, switch, loop….
Plus d’un && ou ||If/else imbriquésCheck NULL
Des commentaires sont nécessaires pour expliquer la logiqueUne complexité élevée (cf sonar)
Des méthodes trop chargées
Comment y remédierDécouper en plusieurs autres méthodesExtraire d’autres classes et déléguerFavoriser le polymorphismeRetourner des objets vides plutôt que des NULLDonner 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
CommandeEtudiantpublic void facturer(Client client) { calculerTotal(TAUX_REDUIT); prelever();}
CommandeStandard public void facturer(Client client) { calculerTotal(TAUX_PLEIN); prelever(); }
Symptômes d’un code intestable
1. Un constructeur cher2. Des instanciations directes3. Des blocs statiques4. Une dynastie de classes5. Des états globaux6. Annuaire de service7. Interroger des collaborateurs8. Des classes hyperactives9. Des méthodes trop chargées10. Mélanger les objets valeurs et les objets services
OpérationgénérerFacture
entrée sortie
Facilement instanciableGetter/SetterAvec un état
Objet valeurEst
Objet serviceFait
Objet valeur / Objet métier
Objet valeur Facile à instancier
Pas de services dans le constructeurOrienté étatProbablement pas d’interfacePas de comportement externe
Objet service Toujours injecté, jamais instanciéSouvent une interfaceSouvent créateur d’objet valeurOrienté serviceA mocker
ClientJoueur
Expresso
BanqueServiceCommandeValidator
BaseDeDonnees
Mélanger les objets valeurs et les objets services
Pourquoi c’est malDevoir tout mockerTests couteux
Comment y remédierExternaliser des classes valeursFaire communiquer les services par des objets valeurs
Symptômes d’un code intestable
Isolabilité Simplicité
Classes hyperactives
Méthodes chargées
Interroger des collaborateurs
Etats globauxAnnuaires
Blocs statiques
Instanciationdirecte
Constructeur cher
Mélangerservice et valeur
Héritage
Vers du code testable
Isolabilité Simplicité
Passer les objets utilisésDirectement 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 veinespour 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éthodesPolymorphisme
Ressources
RéférencesClean 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
LivresxUnit Test PatternsGrowing object oriented softwareWorking effectively with legacy code
Coder proprementRefactoring
Recommended