On va appliquer la méthode TDD pour modéliser une classe d’un domaine métier.
Supposons un domaine de vente d’applications et utilitaires en ligne, la MOA vient d’écrire une story spécifiant en service en ligne, il s’agit de vendre une application mobile innovante.
C’est la page blanche ! Riens n’est encore écris !
Suite à la lecture de la story l’équipe MOE extrait une première classe candidate « produit » identifiée par un code et un nom, une date de mise en ligne dans 6 mois par défaut, de plus le produit est innovant par défaut.
L’article va montrer comment on peut pousser la démarche TDD afin de construire un harnais de test dès le coeur de la modélisation afin d’obtenir un modèle robuste et fiable, indépendamment des services métier et de la couche de persistance.
On pourrais bien sur considérer que coder la classe produit dans un langage comme java est extrêmement trivial et qu’il est non productif de consacrer plus de 5 minutes à cette tâche. Partant de cette idée l’article va tenter de démontrer que le soin apporté aux modèle va en fin de compte non seulement pérenniser la fiabilité des développements à venir mais aussi encourager la mise en place en amont de pratiques qui vont améliorer globalement la productivité.
La session de travail sera réalisée sous eclipse, java 6 et Junit 4.
Pour augmenter la productivité des tests j’ai installé le plugin eclipse MoreUnit : eclipse update
Deux raccourcis très pratiques proposés par More Unit :
Ctrl+J : naviguer de la classe de test à la classe testée et reciproquement
Ctrl+R : lancer les tests à partir de la classe de test ou bien de la classe testée !
C’est parti !
On commence par la création de la classe de test ProduitTest avec Junit 4
Création du premier test « un nouveau produit est innovant »
@Test public void nouveauProduitEstInnovant() new Produit(); }
Création de la classe Produit avec l’ide pour que le test compile et ajout de l’attribut innovant :
private boolean innovant;
Génération des mutateurs/accesseurs avec le raccourci clavier Alt+Maj+S puis retour au test Ctrl+J pour poser l’assertion :
@Test public void nouveauProduitEstInnovant() { //setup Produit produit = new Produit(); //test & assert assertTrue(produit.isInnovant()); }
On exécute notre premier test (Ctrl+R) : c’est rouge ! On corrige la valeur du booléen par défaut :
private boolean innovant = true;
Ctrl+J et Ctrl+R : c’est vert !
En inlinant la variable locale produit c’est plus concis (Alt+Maj+I)
@Test public void nouveauProduitEstInnovant() { //setup & test & assert assertTrue(new Produit().isInnovant()); }
Voila, on a écrit notre premier test.
Deuxième test : « deux produits ayant le même code sont égaux »
Classe Produit :
private String code; //...accesseurs
Classe ProduitTest :
@Test public void deuxProduitMemeCodeSontEgaux() { //setup Produit produitA = new Produit(); produitA.setCode("A"); Produit produitB = new Produit(); produitB.setCode("A"); //test & assert assertEquals(produitA,produitB); }
Ctrl+R : c’est rouge
On génère la méthode equals (Alt+Maj+S) dans la classe Produit en se basant sur l’attribut code :
@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Produit other = (Produit) obj; if (code == null) { if (other.code != null) return false; } else if (!code.equals(other.code)) return false; return true; }
Le test passe au vert mais la méthode equals est relativement complexe et peu lisible si l’on considère que l’on veut juste exprimer que le code est l’identifiant de la classe Produit.
On va améliorer le code en utilisant la classe EqualsBuilder de la librairie apache commons-lang qui garantie la fiabilité des comparaisons des attributs :
@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Produit other = (Produit) obj; return new EqualsBuilder().append(code, other.code).isEquals(); }
On ajoute un test pour le comportement de equals aux limites :
@Test public void equalsAuxLimites() { //setup Produit produit = new Produit(); //pre-assert assertTrue(new Produit(){} instanceof Produit); //test&assert assertTrue(produit.equals(produit)); assertFalse(produit.equals(null)); assertFalse(produit.equals(new Produit(){})); }
Idée de refactoring de la méthode equals : c’est le même comportement aux limites dans le cas général pour tous les objets métiers.
@Override public boolean equals(Object obj) { Boolean isEquals = equalsAuxLimites(obj); if (isEquals != null) return isEquals; else { Produit other = (Produit) obj; return new EqualsBuilder().append(code, other.code).isEquals(); } } //TODO : a externaliser public Boolean equalsAuxLimites(Object obj) { if (this == obj) return true; else if (obj == null) return false; else if (getClass() != obj.getClass()) return false; else return null; }
Autre test portant sur l’identifiant code : « deux produits de code différents ne sont pas égaux »
@Test public void deuxProduitCodesDifferentsNonEgaux() { Produit produitA = new Produit(); produitA.setCode("A"); Produit produitB = new Produit(); produitB.setCode("B"); assertFalse(produitA.equals(produitB)); }
De même que pour la méthode equals on améliore l’écriture de la méthode hashCode grâce à la classe HashCodeBuilder :
@Override public int hashCode() { return new HashCodeBuilder(ODD_NUMBER, 1).append(code).toHashCode(); }
Afin de respecter le contrat général des méthodes hashcode on ajoute un test sur la méthode hashcode : « deux produits égaux ont le même hashcode »
@Test public void deuxProduitsEgauxOntMemeHashCode() { // setup Produit produitA = new Produit(); produitA.setCode("A"); Produit produitABis = new Produit(); produitABis.setCode("A"); // pre-assert assertEquals(produitA, produitABis); assertFalse(produitA == produitABis); // assert assertTrue(produitA.hashCode() == produitABis.hashCode()); }
On ajoute un autre test sur la méthode hashCode pour garantir l’utilisation optimale du produit dans les maps : « deux produits différents ont un hashcode différent » :
@Test public void deuxProduitsDifferentsOntHashCodeDifferents() { //setup Produit produitA = new Produit(); produitA.setCode("A"); Produit produitB = new Produit(); produitB.setCode("B"); //assert assertFalse(produitA.hashCode() == produitB.hashCode()); }
On ajoute ensuite l’attribut nom et ses accesseurs dans la classe Produit et on enrichit les tests de la méthode equals en renseignant le nom du produit.
@Test public void deuxProduitMemeCodeSontEgaux() { Produit produitA = new Produit(); produitA.setCode("A"); produitA.setNom("produit top"); Produit produitAbis = new Produit(); produitAbis.setCode("A"); produitAbis.setNom("produit super"); produitAbis.setInnovant(false); assertEquals(produitA, produitAbis); } @Test public void deuxProduitCodesDifferentsNonEgaux() { Produit produitA = new Produit(); produitA.setCode("A"); produitA.setNom("nom produit A"); Produit produitB = new Produit(); produitB.setCode("B"); produitB.setNom("nom produit A"); assertFalse(produitA.equals(produitB)); }
Nous venons de mettre en place avec TDD les tests essentiels d’une classe du modèle à savoir sa construction et son comportement dans les méthodes equals et hashCode. On a également vu comment quelques idées simples de refactoring permettent de simplifier le code et la testabilité de la classe métier.
Dans la partie 2 de l’article on complétera le modèle et on verra comment améliorer les setup répétitifs des tests…