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…

Publicités