Pratiquer TDD

Michael Borde / www.arpinum.fr / @arpinum

Arpinum

#TDD ?!

TDD By Example XP Explained

La mission ?

Du code propre qui fonctionne...

maintenant!

Deux outils

JUnit & Krang © Fred Wolf Films

Make them first

  • Fast,
  • Isolated,
  • Repeatable,
  • Self-verifying,
  • Timely.

Un cycle rapide

Cycle TDD

Et en pratique ?

Chat pensif

Une vie après le Fizzbuzz ?

Bisounours
© Kenner

Décortiquons la mission

Du code propre qui fonctionne maintenant.

Un premier test


public class ConvertisseurAdresseTest {

  @Test
  public void uneAdresseComplètePeutSAfficherSurUneLigne() {
    Adresse adresse = new Adresse();
    adresse.setRue("31 chemin de Bénédigue");
    adresse.setCodePostal("33400");
    adresse.setVille("Talence");

    String chaine = new ConvertisseurAdresse(adresse).surUneLigne();

    assertThat(chaine).isEqualTo("31 chemin de Bénédigue 33400 Talence");
  }
}
                    

Focus sur le "qui fonctionne"


public class ConvertisseurAdresse {

  public ConvertisseurAdresse(Adresse adresse) {
  }

  public String surUneLigne() {
    return "31 chemin de Bénédigue 33400 Talence";
  }
}
                    

Focus sur le "propre"


public class ConvertisseurAdresse {

  public ConvertisseurAdresse(Adresse adresse) {
    this.adresse = adresse;
  }

  public String surUneLigne() {
    return String.format("%s %s %s", this.adresse.getRue(),
        this.adresse.getCodePostal(), this.adresse.getVille());
  }

  private Adresse adresse;
}
                    

Un deuxième test


@Test
public void uneAdresseVideSAfficheVideSurUneLigne() {
  Adresse adresse = new Adresse();

  String chaine = new ConvertisseurAdresse(adresse).surUneLigne();

  assertThat(chaine).isEqualTo("");
}
                    

Focus sur le "qui fonctionne"


public String surUneLigne() {
  String rue = this.adresse.getRue() != null ? this.adresse.getRue() + " "
      : "";
  String codePostal = this.adresse.getCodePostal() != null ? this.adresse
      .getCodePostal() +
      " " : "";
  String ville = this.adresse.getVille() != null ? this.adresse.getVille()
      : "";
  return String.format("%s%s%s", rue, codePostal, ville);
}
                    

Focus sur le "propre"


public String surUneLigne() {
  List<String> parties = Lists.newArrayList(adresse.getRue(),
    adresse.getCodePostal(), adresse.getVille());
  return Joiner.on(" ").skipNulls().join(parties);
}
                    

TDD découple les activités de recherche de solution et respect de l'excellence technique pour les exécuter au moment le plus pertinent.

Par où commencer ?

Le monde hostile des applications complexes

Machine infernale
© Alice Moisset

La "Clean Architecture"

Clean Architecture
© Uncle Bob

Un point de départ : le modèle du domaine

  • Définit le langage omniprésent,
  • Soulève les questions fonctionnelles,
  • Peu de complexité technique,
  • Tests orientés résultat.

Des tests orientés résultat ?

Un test de chorégraphie


[Test]
public function impossibleDEcouterUnEvenementPlusieursFois():void {
  var ecouteur:EcouteurDEvenement = nice(EcouteurDEvenement);
  _bus.ajouteEcouteur(ecouteur, _evenement);
  _bus.ajouteEcouteur(ecouteur, _evenement);

  _bus.evenementSurvenu(_evenement);

  assertThat(ecouteur, received().method("evenementSurvenu").once());
}
                    

Un test de résultat


[Test]
public function leLancePierreAjusteLeProjectileSiLeToucheSeDeplace():void {
  _touche.deplaceVers(new Point(13, 37));

  _lancePierre.ajusteLeProjectile(_touche);

  assertThat(_projectile.x, equalTo(13));
  assertThat(_projectile.y, equalTo(37));
}
                    

Les doublures de test sont un mal parfois nécessaire.

Problème de test = problème de code de prod.

Corriger des bugs en TDD

Des approches parfois scandaleuses

  • Je corrige à l'aveugle,
  • Je mets des logs partout,
  • Je mets des points d'arrêts partout,
  • J'écris un test.

Un exemple amusant

Un innocent recruteur saisit la description d'offre :

“Vous serez en charge d'un enfant de 5 ans en périscolaire du matin de 6h45 à 7h45 ou en périscolaire du soir de 18h30 à 20h30, 3 à 4 fois par semaine en fonction du planning déterminé par les parents.”

Il obtient le message d'erreur :

“Le numéro de téléphone que vous avez saisi est surtaxé. Nous vous remercions d'indiquer un numéro au tarif local ou un numéro de téléphone mobile.”

Oups

Chat potté
© DreamWorks

Un indice :

“Vous serez en charge d'un enfant de 5 ans en périscolaire du matin de 6h45 à 7h45 ou en périscolaire du soir de 18h30 à 20h30, 3 à 4 fois par semaine en fonction du planning déterminé par les parents.”

En coulisse

Une regex trop envahissante :


public class DetecteurDeNumeroDeTelephone {

  private static final Pattern PATTERN_TELEPHONE =
                         Pattern.compile("(\\d(.){0,3}){9}\\d");

  public List<String> detecte(String texte) {
    List<String> numeros = Lists.newArrayList();
    Matcher m = PATTERN_TELEPHONE.matcher(texte);
        ...
  }
}
                    

Quelques tests


@Test
public void lEnsembleDesNumerosSontDetectesDansUnTexte() {
  String texte = "Voici mon numéro : 06 21 03 43 22, " +
                 "vous pouvez me joindre au travail : 05-22-34-11-45";

  List<String> numeros = this.detecteur.detecte(texte);

  assertThat(numeros).containsExactly("0621034322", "0522341145");
}
                    

@Test
public void unNumeroNePeutPasAvoirDesChiffresSeparesPar3Caracteres() {
  List<String> numeros = this.detecteur.detecte("04---04-04-04---04");

  assertThat(numeros).isEmpty();
}
                    

Des tests auto-vérifiant

Un test peu explicite


@Test
public void unCalendrierPeutDireSiUnJourEstOuvré() {
  Calendrier calendrier = new Calendrier();
  Calendar calendar = Calendar.getInstance();
  calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);

  assertThat(calendrier.estOuvré(calendar.getTime())).isTrue();

  calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
  assertThat(calendrier.estOuvré(calendar.getTime())).isFalse();

  calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
  assertThat(calendrier.estOuvré(calendar.getTime())).isFalse();
}
                    

En cas d'erreur :

org.junit.ComparisonFailure:
Expected: false, Actual: true

      at unCalendrierPeutDireSiUnJourEstOuvré(TestCalendrier.java:23)
                        

Le test auto-vérifiant


@Test
public void unJourDeLaSemaineDeTravailEstOuvré() {
  compareOuvréPourLeJourEtLaValeur(Calendar.MONDAY, true);
}

@Test
public void leSamediNEstPasOuvré() {
  compareOuvréPourLeJourEtLaValeur(Calendar.SATURDAY, false);
}

@Test
public void leDimancheNEstPasOuvré() {
  compareOuvréPourLeJourEtLaValeur(Calendar.SUNDAY, false);
}

private void compareOuvréPourLeJourEtLaValeur(int jour, boolean attendu) {
  boolean ouvré = new Calendrier().estOuvré(créeJour(jour));
  assertThat(ouvré).as("Jour ouvré").isEqualTo(attendu);
}
                    

En cas d'erreur :

org.junit.ComparisonFailure: [Jour ouvré]
Expected: false, Actual: true

       leDimancheNEstPasUnJourOuvré(CalendrierTest.java:24)
                        

Le test doit rester au service du développeur.

Des tests répétables

Un problème de temps?

Alice au pays des merveilles
© Walt Disney Pictures

Un test peu fiable


public class PublieurArticleTest {

  @Test
  public void laDateDePublicationDUnArticleEstLaDateCourante() {
    Article article = new Article();
    new PublieurArticle().publie(article);

    Date datePublication = article.getDatePublication();

    assertThat(datePublication).isEqualTo(new Date());
  }
}
                    

En cas d'erreur :

org.junit.ComparisonFailure: expected: java.lang.String<2013-11-17T18:42:50> but was: java.lang.String<2013-11-17T18:42:50>

Expected :2013-11-17T18:42:50
Actual   :2013-11-17T18:42:50

      at laDateDeCréationDUnArticleEstLaDateCourante(PublieurArticleTest.java:18)
                        

L'implémentation


public class PublieurArticle {

  public void publie(Article article) {
    article.setDatePublication(new Date());
  }
}
                    

@Test
public void laDateDePublicationDUnArticleEstLaDateCourante() {

  ...

  assertThat(datePublication).isEqualTo(new Date());
}
                        

La date courante a évoluée!

Une amélioration : réduire la précision


public class PublieurArticleTest {

  @Test
  public void laDateDePublicationDUnArticleEstLaDateCourante() {
    Article article = new Article();
    new PublieurArticle().publie(article);

    Date datePublication = article.getDatePublication();

    assertThat(datePublication).is(plusOuMoinsMaintenant());
  }
}
                    

Autre piste : découplage du temps


@Test
public void laDateDePublicationDUnArticleEstLaDateCourante() {
  Article article = new Article();
  new PublieurArticle(créeTempsFigé(NOEL)).publie(article);

  Date datePublication = article.getDatePublication();

  assertThat(datePublication).isEqualTo(NOEL);
}

private Temps créeTempsFigé(final Date date) {
  return new Temps() {
    @Override public Date maintenant() {
      return date;
    }
  };
}
                    

Et côté production


public class PublieurArticle {

  public PublieurArticle(Temps temps) {
    this.temps = temps;
  }

  public void publie(Article article) {
    article.setDatePublication(temps.maintenant());
  }

  private Temps temps;
}
                    

public interface Temps {

  Date maintenant();
}
                    

Dans le même ordre d'idée


@Test
public void leFournisseurPeutFournirUnElémentAléatoirement() {
  List<String> chaines = Lists.newArrayList("un", "deux", "trois");
  GenerateurNombre générateur = new GenerateurNombreFige(1);
  FournisseurElement fournisseur = new FournisseurElement(générateur);

  String élément = fournisseur.fournisUnElémentDeLaListe(chaines);

  assertThat(élément).isEqualTo("deux");
}
                    

Les tests doivent rester fiables qu'importe l'environnement.

La qualité des tests est primordiale

La conséquence funeste des mauvais tests

Tests contraignants =>

Moins de tests =>

Moins de feedbacks =>

Moins de courage =>

Moins de refactoring =>

Code moins évolutif =>

Moins d'agilité

Vivement la v2!

Gerbe
© http://www.123fleurs.com

Quelles sont les exigences de qualité des tests?

Une conception simple

  • Le code de test exprime l'intention,
  • Il est concis,
  • Sans duplication.

Les mêmes exigences que pour le code de production !

Un test n'est que du code standard qui a pour seule particularité de ne pas partir en production.

Les questions qu'on ne se pose plus avec TDD

Peut-on tester les méthodes privées ?

Non-sens !

Peut-on mocker les méthodes statiques ?

Non-sens !

Ce qu'on pourrait aborder en off

  • Puis-je créer du code que pour les tests ?
  • Doit-on tout tester ?
  • Les tests bout en bout sont-ils plus fiables ?
  • Comment faire avec du code hérité ?
  • Je n'ai pas le temps de tester
  • Je n'ai pas d'outil dans mon langage
  • ... et bien autres choses

Fin