Test Driven Design

@nphumbert   –   @rnowif

Nadia & Renaud Humbert-Labeaumaz

@nphumbert   –   @rnowif

Testabilité == Maintenabilité

@nphumbert   –   @rnowif

Un code facilement testable doit

  • pouvoir être testé en isolation
  • pouvoir se faire injecter des mocks
  • avoir des tests concis
  • permettre de créer ses propres mocks

@nphumbert   –   @rnowif

Tester en isolation

public class FruitService {

    public boolean isFruitPresentInBasket(Long basketId, Fruit fruit) {
        return new FruitOracleDao()
                    .getFruitsByBasket(basketId)
                    .contains(fruit);
    }
}

@nphumbert   –   @rnowif

Tester en isolation

@Test
public void should_find_fruit_in_basket() {
   boolean fruitPresent = new FruitService()
                            .isFruitPresentInBasket(1L, aBanana());

   // Impossible à vérifier sans vraie base de données Oracle !
   assertThat(fruitPresent).isTrue(); 
}

@nphumbert   –   @rnowif

Tester en isolation

public class FruitService {

    private final FruitRepository fruitRepository;

    public FruitService(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    public boolean isFruitPresentInBasket(Long basketId, Fruit fruit) {
        return fruitRepository
                    .getFruitsByBasket(basketId)
                    .contains(fruit);
    }
}

@nphumbert   –   @rnowif

Tester en isolation

@Test
public void should_find_fruit_in_basket() {
   FruitRepository fruitRepository = mock(FruitRepository.class);
   when(fruitRepository.getFruitsByBasket(1L))
                            .thenReturn(asList(aBanana()));

   boolean fruitPresent = new FruitService(fruitRepository)
                                .isFruitPresentInBasket(1L, aBanana());

   assertThat(fruitPresent).isTrue(); 
}

@nphumbert   –   @rnowif

Tester en isolation

Rendre le code testable en isolation le rend

  • indépendant d'une implémentation particulière grâce à l'injection d'une interface.
  • extensible car n'importe quelle implémentation peut être fournie.

@nphumbert   –   @rnowif

Injecter des mocks

public class FruitService {

    public boolean isHealthy(FoodItem foodItem) {
        Set<Vitamine> vitamines = emptySet();
        if (foodItem instanceof Fruit) {
            vitamines = newHashSet(A, B, C, E);
        } else if (foodItem instanceof Dairy) {
            vitamines = newHashSet(A, B, D);
        } else if (foodItem instanceof Candy) {
            vitamines = emptySet();
        }
        
        return vitamines.size() > 3;
    }
}

@nphumbert   –   @rnowif

Injecter des mocks

@Test
public void should_be_healthy_when_more_than_3_vitamines {
    FoodItem foodItem = mock(FoodItem.class); 
    // Comment indiquer qu'il a plus de 3 vitamines ?

    boolean isHealty = fruitService.isHealthy(foodItem);

    assertThat(isHealty).isTrue();   
}

@nphumbert   –   @rnowif

Injecter des mocks

public class FruitService {

    public boolean isHealthy(FoodItem foodItem) {
        Set<Vitamine> vitamines = foodItem.getVitamines();
        return vitamines.size() > 3;
    }
}

@nphumbert   –   @rnowif

Injecter des mocks

@Test
public void should_be_healthy_when_more_than_3_vitamines {
    FoodItem foodItem = mock(FoodItem.class); 
    when(foodItem.getVitamines()).thenReturn(newHashSet(A, B, C, D));

    boolean isHealty = fruitService.isHealthy(foodItem);

    assertThat(isHealty).isTrue();   
}

@nphumbert   –   @rnowif

Injecter des mocks

Injecter des mocks

  • est possible seulement si le code testé ne varie pas en fonction de l'implémentation de l'interface qu'il manipule.
  • prouve que le code est réutilisable car il est déjà utilisé au moins 2 fois avec des implémentations différentes.

@nphumbert   –   @rnowif

Avoir des tests concis

Réduire le nombre de tests par classe et les garder concis

  • encourage à limiter les responsabilités de la classe testée.
  • réduit le nombre de raisons qu'a la classe de changer.

@nphumbert   –   @rnowif

Créer ses propres mocks

public class FruitService {

    public boolean isHealthy(FoodItem foodItem) {
        return foodItem.getVitamines() > 3;    
    }
}

@nphumbert   –   @rnowif

Créer ses propres mocks

@Test
public void should_be_healthy_when_more_than_3_vitamines {
    FoodItem foodItem = new MockFoodItem(newHashSet(A, B, C, D));

    boolean isHealty = fruitService.isHealthy(foodItem);

    assertThat(isHealty).isTrue();   
}

@nphumbert   –   @rnowif

Créer ses propres mocks

public interface FoodItem {
    
    Set<Vitamine> getVitamines();

    Color getColor();

    Flavor getFlavor();

    Size getSize();

    String getName();

    boolean isOrganic();

    Collection<Allergen> getAllergens();

    boolean isExpired();

    // ...

}

@nphumbert   –   @rnowif

Créer ses propres mocks

@nphumbert   –   @rnowif

Créer ses propres mocks

public class FruitService {

    public boolean isHealthy(Nutrient nutrient) {
        return nutrient.getVitamines() > 3;    
    }
}
public interface Nutrient {
    
    Set<Vitamine> getVitamines();

}

@nphumbert   –   @rnowif

Créer ses propres mocks

@Test
public void should_be_healthy_when_more_than_3_vitamines {
    Nutrient nutrient = () -> newHashSet(A, B, C, D);

    boolean isHealty = fruitService.isHealthy(nutrient);

    assertThat(isHealty).isTrue();   
}

@nphumbert   –   @rnowif

Créer ses propres mocks

Pouvoir créer ses propres mocks

  • encourage à limiter la taille des interfaces.
  • rend le code plus clair et moins sujet aux erreurs car les interfaces plus cohérentes.

@nphumbert   –   @rnowif

Conclusion

Du code facilement testable

  • est extensible et réutilisable car il ne dépend pas de l'implémentation de l'interface qu'il manipule.
  • limite les responsabilités de chaque classe et réduit le nombre de raisons qu'elles ont de changer.
  • limite la taille des interfaces en les rendant plus cohérentes et est moins sujet aux erreurs.

@nphumbert   –   @rnowif

« Le code facilement testable est plus solide et plus maintenable. »

Merci

@nphumbert (nphumbert.github.io)

Crafties @ YouTube

@rnowif (rnowif.github.io)