Comment gérer les classes d'utilitaires statiques lors de la conception de testabilité

63

Nous essayons de concevoir notre système de manière à ce qu'il puisse être testé et, dans la plupart des cas, développé à l'aide de TDD. Nous essayons actuellement de résoudre le problème suivant:

À divers endroits, il est nécessaire d’utiliser des méthodes d’assistance statiques telles que ImageIO et URLEncoder (les deux API standard) ainsi que diverses autres bibliothèques composées principalement de méthodes statiques (comme les bibliothèques Apache Commons). Mais il est extrêmement difficile de tester les méthodes qui utilisent de telles classes d'assistance statique.

J'ai plusieurs idées pour résoudre ce problème:

  1. Utilisez un framework fictif pouvant simuler des classes statiques (comme PowerMock). C'est peut-être la solution la plus simple mais cela donne l'impression d'abandonner.
  2. Créez des classes wrapper instanciables autour de tous ces utilitaires statiques pour pouvoir les injecter dans les classes qui les utilisent. Cela semble être une solution relativement propre, mais je crains que nous ne finissions par créer énormément de ces classes de wrapper.
  3. Extrayez chaque appel de ces classes auxiliaires statiques dans une fonction pouvant être remplacée et testez une sous-classe de la classe que je souhaite tester.

Mais je continue de penser que cela doit être un problème auquel de nombreuses personnes doivent faire face lors de la TDD - il doit donc déjà y avoir des solutions à ce problème.

Quelle est la meilleure stratégie pour garder les classes qui utilisent ces aides statiques testables?

Benedikt
la source
Je ne suis pas sûr de ce que vous entendez par "sources crédibles et / ou officielles" mais je suis d'accord avec ce que @berecursive a écrit dans sa réponse. PowerMock existe pour une raison et cela ne devrait pas donner l'impression d'abandonner, surtout si vous ne voulez pas écrire de classes wrapper vous-même. Les méthodes finales et statiques sont pénibles pour les tests unitaires (et TDD). Personnellement? J'utilise la méthode 2 que vous avez décrite.
Déco
"sources crédibles et / ou officielles" n’est que l’une des options que vous pouvez choisir lorsque vous démarrez une prime pour une question. Ce que je veux dire réellement: expériences ou références à des articles rédigés par des experts de TDD. Ou tout type d'expérience vécue par quelqu'un qui a rencontré le même problème ...

Réponses:

34

(Pas de sources "officielles" ici, je le crains - ce n'est pas comme si on spécifiait comment bien tester. Juste mes opinions, qui seront utiles, espérons-le.)

Lorsque ces méthodes statiques représentent des dépendances authentiques , créez des wrappers. Donc pour des choses comme:

  • ImageIO
  • Clients HTTP (ou tout autre élément lié au réseau)
  • Le système de fichiers
  • Obtenir l'heure actuelle (mon exemple préféré de l'utilisation de l'injection de dépendance)

... il est logique de créer une interface.

Mais beaucoup de méthodes dans Apache Commons ne devraient probablement pas être ridiculisées. Par exemple, prenez une méthode pour réunir une liste de chaînes en ajoutant une virgule entre elles. Il est inutile de s'en moquer - laissez simplement l'appel statique faire son travail normal. Vous ne voulez pas ou n'avez pas besoin de remplacer le comportement normal; vous ne traitez pas avec une ressource externe ou quelque chose avec lequel il est difficile de travailler, ce sont juste des données. Le résultat est prévisible et vous ne voudriez jamais que ce soit autre chose que ce que cela vous donnera de toute façon.

Je soupçonne qu'après avoir supprimé tous les appels statiques qui sont vraiment des méthodes pratiques avec des résultats prévisibles et «purs» (tels que l'encodage en base64 ou URL) plutôt que des points d'entrée dans un vaste gâchis de dépendances logiques (comme HTTP), vous constaterez que c'est entièrement pratique de faire la bonne chose avec les dépendances authentiques.

Jon Skeet
la source
20

C'est certainement une question / réponse d'opinion, mais pour ce que cela vaut, je pensais mettre mes deux sous. Pour ce qui est de la méthode de style TDD 2, c'est certainement l'approche qui suit à la lettre. L'argument en faveur de la méthode 2 est que si vous avez toujours voulu remplacer l'implémentation de l'une de ces classes - disons une ImageIObibliothèque équivalente -, vous pouvez le faire tout en maintenant la confiance dans les classes qui exploitent ce code.

Cependant, comme vous l'avez mentionné, si vous utilisez beaucoup de méthodes statiques, vous finirez par écrire beaucoup de code wrapper. Cela pourrait ne pas être une mauvaise chose à long terme. En termes de maintenabilité, il existe certainement des arguments en ce sens. Personnellement, je préférerais cette approche.

Cela dit, PowerMock existe pour une raison. Il est assez connu que tester des méthodes statiques est très douloureux, d’où le lancement de PowerMock. Je pense que vous devez peser vos options en termes de quantité de travail nécessaire pour emballer toutes vos classes d’aides d’appel par rapport à l’utilisation de PowerMock. Je ne pense pas que ce soit «renoncer» à utiliser PowerMock - je pense simplement que le fait d'encapsuler les classes vous permet plus de flexibilité dans un grand projet. Plus vous avez de contrats publics (interfaces), plus vous pouvez fournir à l'utilisateur la distinction entre l'intention et la mise en œuvre.


la source
1
Une question supplémentaire sur laquelle je ne suis pas vraiment sûr: lors de la mise en oeuvre des wrappers, implémenteriez-vous toutes les méthodes de la classe encapsulée ou juste celles qui sont actuellement nécessaires?
3
En suivant les idées agiles, vous devez faire la chose la plus simple qui fonctionne et éviter de faire un travail inutile. Par conséquent, vous ne devez exposer que les méthodes dont vous avez réellement besoin.
Assaf Stone
@AssafStone accepté
Soyez prudent avec PowerMock, toute la manipulation de classe nécessaire pour simuler les méthodes s'accompagne de beaucoup de temps système. Vos tests seront beaucoup plus lents si vous l'utilisez beaucoup.
bcarlso
Devez-vous vraiment écrire beaucoup si vous associez vos tests / migration à l’adoption d’une bibliothèque DI / IoC?
4

En tant que référence pour tous ceux qui traitent également de ce problème et rencontrent cette question, je vais décrire comment nous avons décidé de le résoudre:

Nous suivons essentiellement le chemin indiqué en n ° 2 (classes de wrapper pour les utilitaires statiques). Mais nous ne les utilisons que lorsqu'il est trop complexe pour fournir à l'utilitaire les données nécessaires pour produire le résultat souhaité (c'est-à-dire lorsque nous devons absolument nous moquer de la méthode).

Cela signifie que nous n'avons pas besoin d'écrire un wrapper pour un utilitaire simple comme Apache Commons StringEscapeUtils(car les chaînes dont ils ont besoin peuvent facilement être fournies) et que nous n'utilisons pas de simulacre pour des méthodes statiques (si nous pensons que nous aurions peut-être besoin de temps, il est temps d'écrire une classe de wrapper puis moquez une instance du wrapper).

Benedikt
la source
2

Je testerais ces classes en utilisant Groovy . Groovy est simple à ajouter à tout projet Java. Avec cela, vous pouvez facilement simuler les méthodes statiques. Voir Mocking Static Methods utilisant Groovy pour un exemple.

trevorisme
la source
1

Je travaille pour une grande compagnie d'assurance et notre code source va jusqu'à 400 Mo de fichiers java purs. Nous avons développé l’application entière sans penser au TDD. À partir de janvier de cette année, nous avons commencé les tests Junit pour chaque composant.

La meilleure solution dans notre département consistait à utiliser des objets fantaisie sur certaines méthodes JNI qui étaient fiables (écrites en C) et, de ce fait, vous ne pouviez pas estimer exactement les résultats à chaque fois sur chaque système d'exploitation. Nous n'avions d'autre choix que d'utiliser des classes simulées et des implémentations spécifiques de méthodes JNI dans le seul but de tester chaque module individuel de l'application pour chaque système d'exploitation que nous prenons en charge.

Mais c’était très rapide et cela a très bien fonctionné jusqu’à présent. Je le recommande - http://www.easymock.org/


la source
1

Les objets interagissent les uns avec les autres pour atteindre un objectif, lorsque vous avez des objets difficiles à tester en raison de l’environnement (point de terminaison de service Web, couche Dao accédant à la base de données, contrôleurs gérant les paramètres de requête http) ou que vous souhaitez tester votre objet isolément, puis vous vous moquez de ces objets.

la nécessité de se moquer des méthodes statiques est une mauvaise odeur, vous devez concevoir votre application plus orientée objet, et les méthodes statiques utilitaires de tests unitaires n’ajoutent pas beaucoup de valeur au projet, la classe wrapper est une bonne approche en fonction de la situation, mais essayez pour tester les objets qui utilisent les méthodes statiques.


la source
1

Parfois j'utilise l'option 4

  1. Utilisez le modèle de stratégie. Créez une classe d'utilitaire avec des méthodes statiques qui délèguent l'implémentation à une instance d'interface enfichable. Codez un initialiseur statique qui se connecte à une implémentation concrète. Branchez une implémentation factice pour les tests.

Quelque chose comme ça.

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

Ce que j'aime dans cette approche, c'est qu'elle garde les méthodes utilitaires statiques, ce qui me semble juste lorsque j'essaie d'utiliser la classe dans le code.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
bigh_29
la source