Comment écrire du code testable ?

foucha 5,007 views 114 slides Jun 02, 2010
Slide 1
Slide 1 of 114
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58
Slide 59
59
Slide 60
60
Slide 61
61
Slide 62
62
Slide 63
63
Slide 64
64
Slide 65
65
Slide 66
66
Slide 67
67
Slide 68
68
Slide 69
69
Slide 70
70
Slide 71
71
Slide 72
72
Slide 73
73
Slide 74
74
Slide 75
75
Slide 76
76
Slide 77
77
Slide 78
78
Slide 79
79
Slide 80
80
Slide 81
81
Slide 82
82
Slide 83
83
Slide 84
84
Slide 85
85
Slide 86
86
Slide 87
87
Slide 88
88
Slide 89
89
Slide 90
90
Slide 91
91
Slide 92
92
Slide 93
93
Slide 94
94
Slide 95
95
Slide 96
96
Slide 97
97
Slide 98
98
Slide 99
99
Slide 100
100
Slide 101
101
Slide 102
102
Slide 103
103
Slide 104
104
Slide 105
105
Slide 106
106
Slide 107
107
Slide 108
108
Slide 109
109
Slide 110
110
Slide 111
111
Slide 112
112
Slide 113
113
Slide 114
114

About This Presentation

(Slides de la présentation à la conférence Agile France 2010)

Vous avez lu la cheatsheet de JMock, la documentation d’EasyMock, la FAQ de Mockito et pourtant, la moitié de votre code n’est toujours pas couvert. Vous n’arrivez juste pas à poser de tests dessus.

Votre code est intestable...


Slide Content

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("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 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("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é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("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

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="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
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("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 dev
Je dev
Je dev
Je dev
Je dev
Je dev
Je dev
Je dev
Mon code
Il dev
Il dev
Il devCOMMIT
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("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 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 = "[email protected]";
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 = "[email protected]";
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