web-dev-qa-db-fra.com

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

Nous essayons de concevoir notre système pour qu'il soit testable et développé dans la plupart des cas à l'aide de TDD. Actuellement, nous essayons de résoudre le problème suivant:

À divers endroits, il est nécessaire pour nous d'utiliser des méthodes d'assistance statiques comme ImageIO et URLEncoder (les deux standards Java API) et diverses autres bibliothèques qui se composent 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 statiques.

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

  1. Utilisez un cadre de simulation qui peut simuler des classes statiques (comme PowerMock). C'est peut-être la solution la plus simple, mais on a l'impression de renoncer.
  2. Créez des classes d'enveloppe instanciables autour de tous ces utilitaires statiques afin qu'ils puissent être injectés dans les classes qui les utilisent. Cela ressemble à une solution relativement propre mais je crains que nous ne finissions par créer énormément de ces classes wrapper.
  3. Extrayez chaque appel à ces classes d'assistance statiques dans une fonction qui peut être remplacée et testez une sous-classe de la classe que je veux réellement tester.

Mais je continue de penser que cela doit juste être un problème auquel beaucoup de gens doivent faire face lorsqu'ils font du TDD - il doit donc déjà y avoir des solutions à ce problème.

Quelle est la meilleure stratégie pour que les classes qui utilisent ces aides statiques puissent être testées?

63
Benedikt

(Pas de sources "officielles" ici, je le crains - ce n'est pas comme s'il y avait une spécification pour bien tester. Juste mes opinions, qui, espérons-le, seront utiles.)

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

  • ImageIO
  • Clients HTTP (ou toute autre chose liée au réseau)
  • Le système de fichiers
  • Obtenir l'heure actuelle (mon exemple préféré où l'injection de dépendance aide)

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

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

Je soupçonne que d'avoir supprimé tous les appels statiques qui sont vraiment des méthodes de commodité avec des résultats prévisibles "purs" (comme le codage base64 ou URL) plutôt que des points d'entrée dans un grand désordre de dépendances logiques (comme HTTP ), vous constaterez qu'il est tout à fait pratique de faire ce qu'il faut avec les véritables dépendances.

34
Jon Skeet

C'est certainement une question/réponse d'opinion mais pour ce que ça vaut, je pensais que je mettrais mes deux cents. En termes de méthode de style TDD 2, c'est certainement l'approche qui la suit à la lettre. L'argument de la méthode 2 est que si vous avez toujours voulu remplacer l'implémentation d'une de ces classes - disons une bibliothèque équivalente ImageIO - alors vous pourriez 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. Ce n'est peut-être pas une mauvaise chose à long terme. En termes de maintenabilité, il y a certainement des arguments pour cela. Personnellement, je préférerais cette approche.

Cela dit, PowerMock existe pour une raison. C'est un problème assez bien connu que tester lorsque des méthodes statiques sont impliquées est très douloureux, d'où la création de PowerMock. Je pense que vous devez évaluer vos options en termes de travail, ce sera pour encapsuler toutes vos classes d'assistance par rapport à l'utilisation de PowerMock. Je ne pense pas que cela "abandonne" l'utilisation de PowerMock - je pense simplement que le fait de regrouper les classes vous permet plus de flexibilité dans un grand projet. Plus vous pouvez fournir de marchés publics (interfaces), plus la séparation entre l'intention et la mise en œuvre est nette.

20
BeRecursive

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

Nous suivons essentiellement le chemin décrit comme # 2 (classes wrapper pour les utilitaires statiques). Mais nous ne les utilisons que lorsqu'elles sont trop complexes pour fournir à l'utilitaire les données requises pour produire la sortie souhaitée (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 être facilement fournies) et nous n'utilisons pas de maquette pour les méthodes statiques (si nous pensons que nous pourrions le faire il est temps d'écrire une classe wrapper puis de se moquer d'une instance du wrapper).

4
Benedikt

Je testerais ces classes en utilisant Groovy . Groovy est simple à ajouter à n'importe quel Java. Avec lui, vous pouvez simuler les méthodes statiques assez facilement. Voir Mocking Static Methods using Groovy pour un exemple.

2
trevorism

Parfois j'utilise l'option 4

  1. Utilisez le modèle de stratégie. Créez une classe utilitaire avec des méthodes statiques qui délèguent l'implémentation à une instance d'interface enfichable. Codez un initialiseur statique qui branche une implémentation concrète. Branchez une implémentation simulée 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 tout le code.

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

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

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

Mais c'était vraiment rapide et ça a plutôt bien fonctionné jusqu'à présent. Je le recommande - http://www.easymock.org/

1
BizzyDizzy

Les objets interagissent les uns avec les autres pour atteindre un objectif, lorsque vous avez un objet difficile à tester en raison de l'environnement (un point de terminaison de service Web, une couche dao accédant à la base de données, des contrôleurs gérant les paramètres de demande http) ou si 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 de l'utilitaire de test unitaire 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.