NUnit – Comment tester toutes les classes qui implémentent une interface particulière

Si j’ai l’interface IFoo et que plusieurs classes l’implémentent, quel est le meilleur moyen / le plus élégant / intelligent de tester toutes ces classes par rapport à l’interface?

J’aimerais réduire la duplication du code de test, tout en restant fidèle aux principes du test unitaire.

Que considéreriez-vous comme meilleure pratique? J’utilise NUnit, mais je suppose que des exemples de n’importe quel framework de test d’unité seraient valides

Si vous avez des classes implémentant une interface, elles doivent toutes implémenter les méthodes de cette interface. Afin de tester ces classes, vous devez créer une classe de test unitaire pour chacune des classes.

Permet de choisir un itinéraire plus intelligent. si votre objective est d’ éviter le code et de tester la duplication de code, vous pouvez créer une classe abstraite prenant en charge le code récurrent .

Par exemple, vous avez l’interface suivante:

public interface IFoo { public void CommonCode(); public void SpecificCode(); } 

Vous voudrez peut-être créer une classe abstraite:

 public abstract class AbstractFoo : IFoo { public void CommonCode() { SpecificCode(); } public abstract void SpecificCode(); } 

Tester c’est facile; implémenter la classe abstraite dans la classe de test soit en tant que classe interne:

 [TestFixture] public void TestClass { private class TestFoo : AbstractFoo { boolean hasCalledSpecificCode = false; public void SpecificCode() { hasCalledSpecificCode = true; } } [Test] public void testCommonCallsSpecificCode() { TestFoo fooFighter = new TestFoo(); fooFighter.CommonCode(); Assert.That(fooFighter.hasCalledSpecificCode, Is.True()); } } 

… ou laissez la classe de test étendre la classe abstraite elle-même si cela vous convient.

 [TestFixture] public void TestClass : AbstractFoo { boolean hasCalledSpecificCode; public void specificCode() { hasCalledSpecificCode = true; } [Test] public void testCommonCallsSpecificCode() { AbstractFoo fooFighter = this; hasCalledSpecificCode = false; fooFighter.CommonCode(); Assert.That(fooFighter.hasCalledSpecificCode, Is.True()); } } 

Avoir une classe abstraite prend en charge le code commun qu’une interface implique, donne une conception de code beaucoup plus propre.

J’espère que cela a du sens pour vous.


En guise de remarque, il s’agit d’un modèle de conception commun appelé modèle . Dans l’exemple ci-dessus, la méthode template est la méthode CommonCode et SpecificCode est appelé un stub ou un hook. L’idée est que tout le monde peut étendre son comportement sans avoir besoin de connaître les éléments cachés.

De nombreux frameworks reposent sur ce modèle de comportement, par exemple ASP.NET où vous devez implémenter les Page_Load dans une page ou des contrôles utilisateur tels que la méthode Page_Load générée appelée par l’événement Load , la méthode template appelle les Page_Load derrière le scènes. Il y a beaucoup plus d’exemples de cela. Fondamentalement, tout ce que vous devez implémenter et qui utilise les mots “load”, “init” ou “render” est appelé par une méthode template.

Je ne suis pas d’accord avec Jon Limjap quand il dit,

Ce n’est pas un contrat sur: a) comment la méthode doit être mise en œuvre et b) ce que cette méthode devrait faire exactement (elle ne garantit que le type de retour), les deux raisons pour lesquelles je glanerais seraient votre motivation à vouloir ce type de test.

De nombreuses parties du contrat pourraient ne pas être spécifiées dans le type de retour. Un exemple agnostique à la langue:

 public interface List { // adds o and returns the list public List add(Object o); // removed the first occurrence of o and returns the list public List remove(Object o); } 

Votre unité teste sur LinkedList, ArrayList, CircularlyLinkedList et tous les autres doivent vérifier que les listes elles-mêmes sont renvoyées mais également qu’elles ont été correctement modifiées.

Il y avait une question précédente sur la conception par contrat, ce qui peut vous aider à trouver la bonne direction pour sécher vos tests.

Si vous ne voulez pas des frais généraux liés aux contrats, je vous recommande des bancs d’essai, inspirés par les recommandations de Spoike :

 abstract class BaseListTest { abstract public List newListInstance(); public void testAddToList() { // do some adding tests } public void testRemoveFromList() { // do some removing tests } } class ArrayListTest < BaseListTest { List newListInstance() { new ArrayList(); } public void arrayListSpecificTest1() { // test something about ArrayLists beyond the List requirements } } 

Je ne pense pas que ce soit la meilleure pratique.

La simple vérité est qu’une interface n’est rien de plus qu’un contrat d’implémentation d’une méthode. Ce n’est pas un contrat sur: a) comment la méthode doit être mise en œuvre et b) ce que cette méthode devrait faire exactement (elle ne garantit que le type de retour), les deux raisons pour lesquelles je glanerais seraient votre motivation à vouloir ce type de test.

Si vous voulez vraiment contrôler l’implémentation de votre méthode, vous avez la possibilité de:

  • L’implémentant en tant que méthode dans une classe abstraite, et hérite de celle-ci. Vous aurez toujours besoin de l’hériter dans une classe concrète, mais vous êtes sûr que si elle n’est pas explicitement remplacée, cette méthode fonctionnera correctement.
  • Dans .NET 3.5 / C # 3.0, implémentation de la méthode en tant que méthode d’extension référençant l’interface

Exemple:

 public static ReturnType MethodName (this IMyinterface myImplementation, SomeObject someParameter) { //method body goes here } 

Toute implémentation faisant correctement référence à cette méthode d’extension émettra précisément cette méthode d’extension. Vous ne devez donc la tester qu’une seule fois.

@Empereur XLII

J’aime le son des tests combinatoires dans MbUnit. J’ai essayé la technique de test d’interface de classe de base abstraite avec NUnit. Bien que cela fonctionne, il vous faudrait un assembly de test séparé pour chaque interface qu’une classe implémente (depuis en C # il n’y a pas d’inheritance multiple – bien que les classes internes puissent être utilisées, ce qui est plutôt cool). En réalité, c’est bien, peut-être même avantageux, car cela regroupe vos tests pour la classe d’implémentation par interface. Mais ce serait bien si le cadre pouvait être plus intelligent. Si je pouvais utiliser un atsortingbut pour marquer une classe en tant que classe de test ‘officielle’ pour une interface, le framework rechercherait l’assembly en cours de test pour toutes les classes qui implémentent l’interface et exécuterait ces tests sur celle-ci.

Ce serait cool.

Que diriez-vous d’une hiérarchie des classes de [TestFixture]? Placez le code de test commun dans la classe de test de base et héritez-le dans les classes de test enfants.

Lors du test d’un contrat d’interface ou de classe de base, je préfère laisser le framework de test se charger automatiquement de la recherche de tous les implémenteurs. Cela vous permet de vous concentrer sur l’interface testée et d’être raisonnablement sûr que toutes les implémentations seront testées, sans avoir à effectuer beaucoup d’implémentation manuelle.

  • Pour xUnit.net , j’ai créé une bibliothèque de résolveur de type afin de rechercher toutes les implémentations d’un type particulier (les extensions xUnit.net constituent simplement une mince couche sur la fonctionnalité de résolveur de type, ce qui permet de l’adapter à d’autres frameworks).
  • Dans MbUnit , vous pouvez utiliser un atsortingbut CombinatorialTest avec les atsortingbuts UsingImplementations sur les parameters.
  • Pour les autres frameworks, le modèle de classe de base mentionné par Spoike peut être utile.

En plus de tester les bases de l’interface, vous devez également vérifier que chaque implémentation respecte ses propres exigences.

Je n’utilise pas NUnit mais j’ai testé les interfaces C ++. Je voudrais d’abord tester une classe TestFoo, qui est une implémentation de base de celle-ci pour vérifier que le matériel générique fonctionne. Ensuite, il vous suffit de tester les éléments propres à chaque interface.