Introduction au test logiciel/Tests et conception

Nous avons vu qu'il pouvait parfois être difficile de tester du code, comme dans la section « Limites des doublures » si la structure de celui-ci ne s'y prêtait pas. Dans le présent chapitre, nous allons voir les bonnes et les mauvaises pratiques qu'il faut connaître lorsqu'on écrit du code afin de rendre celui-ci testable. On utilise parfois le terme « testabilité ».

Faciliter l'instanciation

modifier

Nous l'avons vu au premier chapitre sur les tests unitaires : tester, c'est exécuter le code et donc instancier. Aussi si une classe est difficile à instancier, cela complique le test. Si le constructeur de la classe a beaucoup de paramètres, il faudra créer autant d'objets et eux-mêmes peuvent nécessiter encore d'autres instances. Tout cela peut aboutir à un cauchemar d'instanciation.

S'il est si difficile d'instancier une classe, il y a peut-être un problème de conception. Est-ce que la Loi de Déméter est respectée ? Est ce qu'il n'y a pas un problème de séparation des préoccupations ? Si cette classe a besoin d'autant de dépendances, ne fait-elle pas trop de choses ? Ne devrait-elle pas déléguer une partie du travail ?

Certaines questions peuvent être résolues en respectant certains patrons de conceptions, notamment les patrons GRASP définissant la répartitions des responsabilités des classes d'un logiciel (voir en particulier le faible couplage et la forte cohésion).

Permettre l'utilisation de doublures

modifier

Design par interfaces

modifier

Décrivez les principaux composants par leurs contrats, c'est à dire par des interfaces. Il est plus facile de créer un bouchon ou un substitut quand c'est une interface qui est attendue plutôt qu'une classe.

Sous-classes

modifier

Avec la programmation orientée objet il ne faut pas hésiter à subdiviser la hiérarchie d'une classe afin de concevoir des classes simples à utiliser et donc à tester.

Quand une interface possède plusieurs classes d'implémentation, il peut être utile de créer une classe parente commune à toutes ces classes afin d'y implémenter les comportements communs et faciliter également l'ajout éventuel d'une nouvelle classe d'implémentation. Cela éviter de répéter du code plusieurs fois et permet de tester une fois pour toutes les méthodes communes à ces classes.

Permettre l'injection de dépendances

modifier

Nous avons vu que certaines classes peuvent rendre difficile, voire impossible, l'utilisation de doublures.

/* comment remplacer attributPrive par sa doublure pour tester
 * methode() ? Dans ce cas, ce n'est pas possible.
 */
public class ClasseTestee {

    private final UneClasseQuelconque attributPrive;

    public ClasseTestee() {
        attributPrive = new UneClasseQuelconque();
    }

    public methode() {
        // code qui fait appel a attributPrive
    }
}

Le problème vient du fait que la classe qui détient le comportement est également responsable de l'instanciation. Ici, il y a deux solutions pour permettre l'injection de dépendance. La première est de ne pas instancier dans le constructeur mais de demander à l'appelant de passer l'objet en argument du constructeur. Toutefois, si ce constructeur est appelé à différents endroits dans le reste de l'application, il faudra dupliquer le code d'instanciation (on perd en factorisation). La deuxième solution est de garder ce constructeur et d'en créer un autre, qui, lui, permet d'injecter la dépendance. Ainsi, l'application utilisera le premier tandis que dans les tests, ou pourra utiliser le deuxième pour injecter la doublure.

public class ClasseTestee {

    private final UneClasseQuelconque attributPrive;

    public ClasseTestee(UneClasseQuelconque uneClasseQuelconque) {
        attributPrive = uneClasseQuelconque;
    }

    public ClasseTestee() {
        this(new UneClasseQuelconque());
    }

    public methode() {
        // code qui fait appel a attributPrive
    }
}

Permettre l'isolation

modifier

Éviter les états globaux

modifier

Les états globaux sont des données partagées de façon transverses dans toute l'application. Typiquement, il s'agit d'instances de type singletons ou variables globales.

Permettre l'observation

modifier
  • Permettre la redéfinition par héritage
  • Laisser la structure interne visible (pas de champs private, mais plutôt protected). Indiquer ce qui a été rendu visible uniquement à des fins de testabilité. Par exemple, les ingénieurs de Google ont choisi d'ajouter l'annotation @VisibleForTesting dans l'API de Guava.
  • Regardez ce que retourne vos méthodes. Les objets retournés permettent-ils de vérifier que tout s'est bien passé ?