Modèles ou pratiques pour les méthodes de test unitaire 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. Prenez le code suivant par exemple:

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

Je comprends que ce soit un mauvais exemple, mais il existe trois appels de méthode statiques qui sont tous légèrement différents. La fonction File.Create accède au système de fichiers et cette fonction ne m’appartient pas. MyUtilities.GetFormattedText est une fonction que je possède et qui est purement sans état. Enfin, MyUtilities.WriteTextToFile est une fonction que je possède et qui accède au système de fichiers.

Ce que je me demandais dernièrement, s’il s’agissait d’un code hérité, comment pourrais-je le reformuler pour le rendre plus testable? J’ai entendu plusieurs arguments selon lesquels les fonctions statiques ne devraient pas être utilisées car elles sont difficiles à tester. Je ne suis pas d’accord avec cette idée car les fonctions statiques sont utiles et je ne pense pas qu’un outil utile devrait être mis au rebut simplement parce que le cadre de test utilisé ne le gère pas très bien.

Après de nombreuses recherches et délibérations, je suis parvenu à la conclusion qu’il est fondamentalement possible d’utiliser 4 modèles ou pratiques pour rendre des fonctions appelant des fonctions statiques unitaires testables. Ceux-ci incluent les suivants:

  1. Ne vous moquez pas du tout de la fonction statique et laissez simplement 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’dependency injection pour l’utiliser dans votre classe. Je parlerai d’ dependency injection d’interface .
  3. Utilisez Moles (ou TypeMock) pour pirater l’appel de fonction.
  4. Utilisez l’injection dépendante pour la fonction. Je me référerai à cela comme dependency injection de fonction .

J’ai entendu beaucoup de discussions sur les trois premières pratiques, mais alors que je réfléchissais à des solutions à ce problème, la quasortingème idée me vint de l’ dependency injection de fonction . Cela revient à cacher une fonction statique derrière une interface, mais sans avoir réellement besoin de créer une interface et une classe wrapper. Un exemple de ceci serait le suivant:

 public class MyInstanceClass { private Action writeFunction = delegate { }; public MyInstanceClass(Action functionDependency) { writeFunction = functionDependency; } public void DoSomething2() { using (FileStream fStream = File.Create(@"C:\test.txt")) { ssortingng text = MyUtilities.GetFormattedText("hello world"); writeFunction(text, fStream); } } } 

Parfois, créer une interface et une classe wrapper pour un appel de fonction statique peut s’avérer fastidieux et polluer votre solution avec de nombreuses petites classes dont le seul but est d’appeler une fonction statique. Je suis tout à fait en faveur de l’écriture d’un code facilement testable, mais cette pratique semble être une solution de contournement pour un mauvais framework de test.

Tandis que je réfléchissais à ces différentes solutions, je me suis rendu compte que chacune des 4 pratiques mentionnées ci-dessus peut être appliquée dans différentes situations. Voici ce que je pense être les bonnes circonstances pour appliquer les pratiques ci-dessus :

  1. Ne vous moquez pas de la fonction statique si elle est purement sans état et n’accède pas aux ressources système (telles que le système de fichiers ou une firebase database). Bien sûr, l’argument peut être avancé que si l’access aux ressources système est introduit, cela introduit de toute façon un état dans la fonction statique.
  2. Utilisez l’ dependency injection d’interface lorsque vous utilisez plusieurs fonctions statiques qui peuvent toutes être logiquement ajoutées à une seule interface. La clé ici est que plusieurs fonctions statiques sont utilisées. Je pense que dans la plupart des cas, ce ne sera pas le cas. Il n’y aura probablement qu’une ou deux fonctions statiques appelées dans une fonction.
  3. Utilisez Moles lorsque vous modifiez des bibliothèques externes telles que des bibliothèques d’interface utilisateur ou des bibliothèques de bases 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 se moquer de votre propre code, cela indique que certaines opérations de refactorisation doivent être effectuées pour découpler les objects.
  4. Utilisez l’ dependency injection de fonction lorsqu’il existe un petit nombre d’appels de fonction statiques dans le code en cours de test. C’est le motif sur lequel je me penche dans la plupart des cas afin de tester les fonctions qui appellent des fonctions statiques dans mes propres classes d’utilitaires.

Ce sont mes pensées, mais j’apprécierais vraiment quelques retours à ce sujet. Quel est le meilleur moyen de tester le code lorsqu’une fonction statique externe est appelée?

L’utilisation de l’dependency injection (option 2 ou 4) est définitivement la méthode que je préfère utiliser. Non seulement cela facilite les tests, mais cela permet également de séparer les problèmes et d’empêcher que les cours ne soient trop gonflés.

Une clarification que j’ai besoin d’apporter est qu’il n’est pas vrai que les méthodes statiques sont difficiles à tester. Le problème avec les méthodes statiques se produit 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 habituel de ceci est avec I / O. Dans votre exemple, vous écrivez du texte dans un fichier (WriteTextToFile). Et si quelque chose devait échouer lors de cette méthode? Étant donné que 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 de WriteTextToFile et le simuler des erreurs. Oui, vous aurez quelques interfaces et classes supplémentaires, mais normalement, vous pouvez regrouper logiquement des fonctions similaires dans une classe.

Sans dependency injection: Il s’agit à peu près de l’option 1 où rien n’est moqué. Je ne vois pas cela comme une stratégie solide, car cela ne vous permet pas de procéder à des tests approfondis.

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

Avec dependency injection:

 public void WriteMyFile(IFileRepository aRepository){ try{ using (FileStream fStream = aRepository.Create(@"C:\test.txt")){ ssortingng 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. } } 

En revanche, souhaitez-vous que vos tests de logique métier échouent si le système de fichiers / la firebase database ne peut pas être lu / écrit? Si nous vérifions que le calcul est correct dans notre calcul de salaire, nous ne voulons pas que les erreurs d’entrée-sortie entraînent l’échec du test.

Sans dependency injection:

C’est un peu un exemple / une méthode étrange mais je ne l’utilise que 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 dependency injection:

 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 se moquer. Les E / S sont coûteuses et leur réduction augmentera la vitesse de vos tests. Ne pas avoir à attendre une transaction de firebase database 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 beaucoup. Mon impression, cependant, est la même que la vôtre: si vous devez l’utiliser, vous pourrez probablement procéder à une refactorisation.

Bienvenue aux maux de l’état statique.

Je pense que vos directives sont OK, dans l’ensemble. Voici mes pensées:

  • Tester à l’unité toute “fonction pure”, qui ne produit pas d’effets secondaires, convient sans distinction de visibilité et de scope. Ainsi, tester des méthodes d’extension statique telles que “Linq helpers” et le formatage de chaînes en ligne (comme les wrappers pour Ssortingng.IsNullOrEmpty ou Ssortingng.Format) et d’autres fonctions utilitaires sans état est tout à fait approprié.

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

  • Si vous devez simplement implémenter un vrai singleton, envisagez de protéger le constructeur par défaut au lieu de le rendre totalement privé et définissez un “proxy test” qui dérive de votre instance singleton et permet la création de l’object dans l’instance d’instance. Cela permet de générer une “simulation partielle” pour toutes les méthodes entraînant des effets secondaires.

  • Si votre code fait référence à des statiques intégrées (telles que ConfigurationManager) qui ne sont pas essentielles au fonctionnement de la classe, extrayez les appels statiques dans une dépendance distincte que vous pouvez simuler ou recherchez une solution basée sur une instance. Bien entendu, toute statique intégrée peut être testée sur plusieurs unités, mais l’utilisation de votre infrastructure de tests unitaires (MS, NUnit, etc.) ne présente aucun inconvénient pour la construction de tests d’intégration. Il suffit de les séparer pour pouvoir exécuter des tests unitaires sans avoir besoin de environnement personnalisé.

  • Partout où le code fait référence à la statique (ou a d’autres effets secondaires) et qu’il est impossible de 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 «modèle factice» qui remplace la méthode. .

Créez simplement un test unitaire pour la méthode statique et n’hésitez pas à l’appeler à l’intérieur de méthodes pour tester sans le simuler.

Pour File.Create et MyUtilities.WriteTextToFile , je MyUtilities.WriteTextToFile mon propre wrapper et l’injecterais avec l’dependency injection. Puisqu’il touche le système de fichiers, ce test pourrait ralentir à cause des E / S et peut-être même générer une exception inattendue du système de fichiers qui vous ferait penser que votre classe est incorrecte, mais c’est maintenant.

En ce qui concerne la fonction MyUtilities.GetFormattedText , je suppose que cette fonction ne fait que quelques modifications avec la chaîne, rien d’inquiétant ici.

Le choix n ° 1 est le meilleur. Ne vous moquez pas, et utilisez simplement la méthode statique telle qu’elle existe. C’est l’itinéraire le plus simple et fait exactement ce dont vous avez besoin. Les deux scénarios «d’injection» continuent d’appeler la méthode statique, vous ne gagnez donc rien grâce aux enveloppements supplémentaires.