39 votes

Modèles ou pratiques pour les tests unitaires des méthodes qui appellent une méthode statique

Dernièrement, j'ai beaucoup réfléchi à la meilleure façon de "simuler" une méthode statique appelée à partir d'une classe que j'essaie de tester. Prenons l'exemple du code suivant :

using (FileStream fStream = File.Create(@"C:\test.txt"))
{
    string text = MyUtilities.GetFormattedText("hello world");
    MyUtilities.WriteTextToFile(text, fStream);
}

Je comprends qu'il s'agit d'un mauvais exemple, mais il comporte trois appels de méthodes statiques qui sont toutes légèrement différentes. La fonction File.Create accède au système de fichiers et je ne possède pas cette fonction. La fonction MyUtilities.GetFormattedText est une fonction qui m'appartient et qui est purement statique. Enfin, la fonction MyUtilities.WriteTextToFile est une fonction qui m'appartient et qui accède au système de fichiers.

Ce à quoi j'ai réfléchi dernièrement, c'est que s'il s'agissait d'un code hérité, comment pourrais-je le remanier pour le rendre plus testable à l'unité. J'ai entendu plusieurs arguments selon lesquels les fonctions statiques ne devraient pas être utilisées parce qu'elles sont difficiles à tester. Je ne suis pas d'accord avec cette idée parce que les fonctions statiques sont utiles et je ne pense pas qu'un outil utile doive être rejeté simplement parce que le cadre de test utilisé ne peut pas le gérer très bien.

Après de longues recherches et délibérations, je suis parvenu à la conclusion qu'il y a essentiellement 4 modèles ou pratiques qui peut être utilisée pour rendre les fonctions qui appellent des fonctions statiques testables à l'unité. Il s'agit notamment des éléments suivants :

  1. Ne vous moquez pas la fonction statique et laisser le test unitaire l'appeler.
  2. Enveloppez la méthode statique dans une classe d'instance qui implémente une interface avec la fonction dont vous avez besoin, puis utilisez l'injection de dépendance pour l'utiliser dans votre classe. C'est ce que j'appellerai injection de dépendance d'interface .
  3. Utilisation Taupes (ou TypeMock) pour détourner l'appel de fonction.
  4. Utiliser l'injection de dépendance pour la fonction. Je l'appellerai l'injection de dépendances de fonctions .

J'ai beaucoup entendu parler des trois premières pratiques, mais alors que je réfléchissais à des solutions à ce problème, la quatrième idée m'est venue, à savoir l'injection de dépendances de fonctions . Cela revient à cacher une fonction statique derrière une interface, mais sans qu'il soit nécessaire de créer une interface et une classe enveloppante. Voici un exemple de cette méthode :

public class MyInstanceClass
{
    private Action<string, FileStream> writeFunction = delegate { };

    public MyInstanceClass(Action<string, FileStream> functionDependency)
    {
        writeFunction = functionDependency;
    }

    public void DoSomething2()
    {
        using (FileStream fStream = File.Create(@"C:\test.txt"))
        {
            string text = MyUtilities.GetFormattedText("hello world");
            writeFunction(text, fStream);
        }
    }
}

Parfois, la création d'une interface et d'une classe enveloppante pour l'appel d'une fonction statique peut s'avérer fastidieuse et polluer votre solution avec un grand nombre de petites classes dont le seul but est d'appeler une fonction statique. Je suis tout à fait favorable à l'écriture d'un code facilement testable, mais cette pratique semble être une solution de contournement pour un mauvais cadre de test.

En réfléchissant à ces différentes solutions, j'ai compris que les quatre pratiques mentionnées ci-dessus peuvent être appliquées dans différentes situations. Voici ce que je pense être la les circonstances correctes pour appliquer les pratiques susmentionnées :

  1. Ne vous moquez pas la fonction statique si elle est purement sans état et n'accède pas aux ressources du système (telles que le système de fichiers ou une base de données). Bien entendu, on peut faire valoir que si l'on accède aux ressources du système, cela introduit de toute façon de l'état dans la fonction statique.
  2. Utilisation l'injection de dépendance d'interface lorsque vous utilisez plusieurs fonctions statiques qui peuvent logiquement être ajoutées à une interface unique. La clé ici est qu'il y a plusieurs fonctions statiques utilisées. Je pense que dans la plupart des cas, ce n'est pas le cas. Il n'y aura probablement qu'une ou deux fonctions statiques appelées dans une fonction.
  3. Utilisation Taupes lorsque vous mockez des bibliothèques externes telles que des bibliothèques d'interface utilisateur ou des bibliothèques de base de données (telles que linq to sql). Mon opinion est que si Moles (ou TypeMock) est utilisé pour détourner le CLR afin de simuler votre propre code, alors c'est un indicateur qu'un refactoring doit être fait pour découpler les objets.
  4. Utilisation l'injection de dépendances de fonctions lorsqu'il y a un petit nombre d'appels de fonctions statiques dans le code testé. C'est le modèle vers lequel je me tourne dans la plupart des cas pour tester les fonctions qui appellent des fonctions statiques dans mes propres classes utilitaires.

Ce sont là mes réflexions, mais j'apprécierais vraiment d'avoir un retour d'information à ce sujet. Quelle est la meilleure façon de tester un code dans lequel une fonction statique externe est appelée ?

19voto

brainimus Points 2578

L'utilisation de l'injection de dépendances (option 2 ou 4) est sans aucun doute la méthode que je préfère pour résoudre ce problème. Non seulement elle facilite les tests, mais elle permet aussi de séparer les préoccupations et d'éviter que les classes ne deviennent trop volumineuses.

Je dois cependant préciser qu'il n'est pas vrai que les méthodes statiques sont difficiles à tester. Le problème des méthodes statiques survient lorsqu'elles sont utilisées dans une autre méthode. Cela rend la méthode qui appelle la méthode statique difficile à tester car la méthode statique ne peut pas être simulée. L'exemple le plus courant est celui des entrées-sorties. Dans votre exemple, vous écrivez du texte dans un fichier (WriteTextToFile). Que se passe-t-il si quelque chose échoue au cours de cette méthode ? Puisque la méthode est statique et qu'elle ne peut pas être simulée, vous ne pouvez pas créer à la demande des cas tels que des cas d'échec. Si vous créez une interface, vous pouvez simuler l'appel à WriteTextToFile et simuler des erreurs. Oui, vous aurez un peu plus d'interfaces et de classes, mais normalement vous pouvez regrouper des fonctions similaires dans une seule classe.

Sans injection de dépendance : C'est à peu près l'option 1, où l'on ne se moque de rien. Je ne considère pas qu'il s'agisse d'une stratégie solide, car elle ne permet pas de réaliser des tests approfondis.

public void WriteMyFile(){
    try{
        using (FileStream fStream = File.Create(@"C:\test.txt")){
            string text = MyUtilities.GetFormattedText("hello world");
            MyUtilities.WriteTextToFile(text, fStream);
        }
    }
    catch(Exception e){
        //How do you test the code in here?
    }
}

Avec l'injection de dépendance :

public void WriteMyFile(IFileRepository aRepository){
    try{
        using (FileStream fStream = aRepository.Create(@"C:\test.txt")){
            string text = MyUtilities.GetFormattedText("hello world");
            aRepository.WriteTextToFile(text, fStream);
        }
    }
    catch(Exception e){
        //You can now mock Create or WriteTextToFile and have it throw an exception to test this code.
    }
}

D'un autre côté, voulez-vous que vos tests de logique d'entreprise échouent si le système de fichiers/la base de données ne peut pas être lu/écrit ? Si nous testons que le calcul de notre salaire est correct, nous ne voulons pas que des erreurs d'E/S fassent échouer le test.

Sans injection de dépendance :

Il s'agit d'un exemple ou d'une méthode un peu étrange, mais je l'utilise uniquement pour illustrer mon propos.

public int GetNewSalary(int aRaiseAmount){
    //Do you really want the test of this method to fail because the database couldn't be queried?
    int oldSalary = DBUtilities.GetSalary(); 
    return oldSalary + aRaiseAmount;
}

Avec l'injection de dépendance :

public int GetNewSalary(IDBRepository aRepository,int aRaiseAmount){
    //This call can now be mocked to always return something.
    int oldSalary = aRepository.GetSalary();
    return oldSalary + aRaiseAmount;
}

L'augmentation de la vitesse est un avantage supplémentaire de la moquerie. Les entrées-sorties sont coûteuses et la réduction des entrées-sorties augmentera la vitesse de vos tests. Ne pas avoir à attendre une transaction de base de données ou une fonction de système de fichiers améliorera les performances de vos tests.

Je n'ai jamais utilisé TypeMock, je ne peux donc pas en parler. Mon impression est la même que la vôtre, à savoir que si vous devez l'utiliser, il y a probablement du refactoring à faire.

15voto

KeithS Points 36130

Bienvenue dans les maux de l'État statique.

Je pense que vos lignes directrices sont bonnes, dans l'ensemble. Voici ce que j'en pense :

  • Le test unitaire de toute "fonction pure", qui ne produit pas d'effets secondaires, est acceptable, quelles que soient la visibilité et la portée de la fonction. Ainsi, le test unitaire des méthodes d'extension statiques telles que les "Linq helpers" et le formatage de chaînes en ligne (comme les wrappers pour String.IsNullOrEmpty ou String.Format) et d'autres fonctions utilitaires sans état est tout à fait acceptable.

  • Les singletons sont l'ennemi d'un bon test unitaire. Au lieu d'implémenter directement le modèle du singleton, envisagez d'enregistrer les classes que vous souhaitez restreindre à une seule instance dans un conteneur IoC et de les injecter dans les classes dépendantes. Les mêmes avantages, avec l'avantage supplémentaire que le conteneur IoC peut être configuré pour renvoyer un simulacre dans vos projets de test.

  • Si vous devez impérativement implémenter un vrai singleton, envisagez de rendre le constructeur par défaut protégé au lieu d'être entièrement privé, et définissez un "proxy de test" qui dérive de votre instance de singleton et permet la création de l'objet dans la portée de l'instance. Cela permet de générer un "simulacre partiel" pour toutes les méthodes qui ont des effets secondaires.

  • Si votre code fait référence à des éléments statiques intégrés (tels que ConfigurationManager) qui ne sont pas fondamentaux pour le fonctionnement de la classe, vous pouvez soit extraire les appels statiques dans une dépendance séparée dont vous pouvez vous moquer, soit rechercher une solution basée sur les instances. Évidemment, toute statique intégrée n'est pas testable unitairement, mais il n'y a pas de mal à utiliser votre framework de test unitaire (MS, NUnit, etc) pour construire des tests d'intégration, gardez-les simplement séparés afin de pouvoir exécuter des tests unitaires sans avoir besoin d'un environnement personnalisé.

  • Lorsque le code fait référence à des éléments statiques (ou a d'autres effets secondaires) et qu'il n'est pas possible de le refactoriser dans une classe complètement séparée, extraire l'appel statique dans une méthode et tester toutes les autres fonctionnalités de la classe à l'aide d'un "simulacre partiel" de cette classe qui surcharge la méthode.

1voto

STO Points 4597

Il suffit de créer un test unitaire pour la méthode statique et de l'appeler à l'intérieur des méthodes pour la tester sans la simuler.

1voto

oenning Points 3679

Pour les File.Create y MyUtilities.WriteTextToFile Je créerais mon propre wrapper et l'injecterais avec l'injection de dépendances. Puisqu'il touche le système de fichiers, ce test pourrait ralentir à cause des entrées-sorties et peut-être même lever une exception inattendue du système de fichiers, ce qui vous ferait penser que votre classe est erronée, mais ce n'est pas le cas.

En ce qui concerne les MyUtilities.GetFormattedText Je suppose que cette fonction ne fait que modifier la chaîne de caractères, il n'y a pas lieu de s'inquiéter.

0voto

A.R. Points 5329

Le choix n° 1 est le meilleur. Ne vous moquez pas et utilisez simplement la méthode statique telle qu'elle existe. C'est le chemin le plus simple et il fait exactement ce que vous voulez qu'il fasse. Vos deux scénarios d'"injection" appellent toujours la méthode statique, donc vous ne gagnez rien avec tout cet habillage supplémentaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X