114
Comment écrire du code testable Conférence Agile France 2010 Florence CHABANOIS

Comment écrire du code testable ?

  • Upload
    fou-cha

  • View
    4.669

  • Download
    2

Embed Size (px)

Citation preview

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

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 globauxAnnuaires

Blocs statiques

Instanciationdirecte

Constructeur cher

Mélangerservice 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 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();(…)

}

Symptômes d’un code intestable

1. Un constructeur cher

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

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

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

1. Un constructeur cher2. Des instanciations directes

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

?

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

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 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

Pollueurs

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 = "[email protected]"; 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 = "[email protected]"; 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

CommandeManager

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

Service

Valeur

ServiceValeur

Service

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

Outils

Singleton detectorTestability explorer

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