6

Je commence à m'intéresser aux tests unitaires, à l'injection de dépendances et à tout ce qui concerne le jazz tout en construisant mon dernier projet ASP.NET MVC.Comment pouvez-vous tester vos contrôleurs sans conteneur IoC?

Je suis au point maintenant où je voudrais tester mes contrôleurs unitaires et j'ai de la difficulté à trouver comment le faire de façon appropriée sans un conteneur IoC.

Prenez par exemple un contrôleur simple:

public class QuestionsController : ControllerBase 
{ 
    private IQuestionsRepository _repository = new SqlQuestionsRepository(); 

    // ... Continue with various controller actions 
} 

Cette classe n'est pas unité très testable en raison de sa instanciation directe de SqlQuestionsRepository. Alors, laisse aller dans le sens d'injection et faire Dépendance:

public class QuestionsController : ControllerBase 
{ 
    private IQuestionsRepository _repository; 

    public QuestionsController(IQuestionsRepository repository) 
    { 
     _repository = repository; 
    } 
} 

Cela semble mieux. Je peux maintenant facilement écrire des tests unitaires avec un faux IQuestionsRepository. Cependant, qu'est-ce qui va instancier le contrôleur maintenant? Quelque part plus haut dans la chaîne d'appel, SqlQuestionRepository va devoir être instancié. Il semble que j'ai simplement déplacé le problème ailleurs, sans m'en débarrasser. Maintenant, je sais que c'est un bon exemple où un conteneur IoC peut vous aider en câblant les dépendances des contrôleurs pour moi tout en gardant mon contrôleur facilement testable.

Ma question est, comment est-on supposé faire des tests unitaires sur des choses de cette nature sans un conteneur IoC?

Note: Je ne suis pas contre les conteneurs IoC, et je vais probablement le faire bientôt. Cependant, je suis curieux de savoir quelle est l'alternative pour les personnes qui ne les utilisent pas.

Répondre

2

N'est-il pas possible de conserver l'instanciation directe du champ et de fournir également le setter? Dans ce cas, vous n'appelez le setter que pendant les tests unitaires. Quelque chose comme ceci:

public class QuestionsController : ControllerBase 
{ 
    private IQuestionsRepository _repository = new SqlQuestionsRepository(); 

    // Really only called during unit testing... 
    public QuestionsController(IQuestionsRepository repository) 
    { 
     _repository = repository; 
    } 
} 

Je suis pas trop familier avec .NET mais comme une note de côté en Java c'est une façon commune à factoriser le code existant pour améliorer la testabilité. I.E., si vous avez des classes qui sont déjà utilisées et que vous avez besoin de les modifier afin d'améliorer la couverture de code sans casser les fonctionnalités existantes.

Notre équipe l'a fait auparavant, et nous fixons généralement la visibilité du setter à package-private et gardons le même paquet de la classe de test afin qu'il puisse appeler le setter.

+0

@Peter, très bonne suggestion. Avez-vous déjà utilisé des conteneurs IoC sur vos projets ou n'avez-vous pas encore trouvé de besoin? – mmcdole

+0

Je n'ai pas beaucoup d'expérience .NET, la plupart de mon travail est avec Java en utilisant Spring pour IoC. Cela a été très utile pour améliorer la modularité. – Peter

+0

@Peter: C'est à peu près le modèle que j'utilise. J'ai posté une réponse qui fait la même chose, mais qui utilise des fonctionnalités de langage C# que vous, en tant que Java, n'êtes peut-être pas au courant. –

2

Vous pourriez avoir un constructeur par défaut avec votre contrôleur qui aura un comportement par défaut.

Quelque chose comme ...

public QuestionsController() 
    : this(new QuestionsRepository()) 
{ 
} 

De cette façon, par défaut lorsque l'usine de contrôleur crée une nouvelle instance du contrôleur, il utilisera le comportement du constructeur par défaut. Ensuite, dans vos tests unitaires, vous pourriez utiliser un cadre de simulation pour passer un simulacre dans l'autre constructeur.

1

Une option consiste à utiliser des contrefaçons.

public class FakeQuestionsRepository : IQuestionsRepository { 
    public FakeQuestionsRepository() { } //simple constructor 
    //implement the interface, without going to the database 
} 

[TestFixture] public class QuestionsControllerTest { 
    [Test] public void should_be_able_to_instantiate_the_controller() { 
     //setup the scenario 
     var repository = new FakeQuestionsRepository(); 
     var controller = new QuestionsController(repository); 
     //assert some things on the controller 
    } 
} 

Une autre option consiste à utiliser des images fantaisie et un cadre de simulation, qui peut générer automatiquement ces images à la volée.

[TestFixture] public class QuestionsControllerTest { 
    [Test] public void should_be_able_to_instantiate_the_controller() { 
     //setup the scenario 
     var repositoryMock = new Moq.Mock<IQuestionsRepository>(); 
     repositoryMock 
      .SetupGet(o => o.FirstQuestion) 
      .Returns(new Question { X = 10 }); 
     //repositoryMock.Object is of type IQuestionsRepository: 
     var controller = new QuestionsController(repositoryMock.Object); 
     //assert some things on the controller 
    } 
} 

En ce qui concerne l'endroit où tous les objets sont construits. Dans un test unitaire, vous ne configurez qu'un ensemble minimal d'objets: un objet réel en cours de test et certaines dépendances simulées ou simulées que l'objet réel testé nécessite. Par exemple, l'objet réel sous test est une instance de QuestionsController - il a une dépendance sur IQuestionsRepository, donc nous lui donnons soit un faux IQuestionsRepository comme dans le premier exemple ou un faux IQuestionsRepository comme dans le second exemple.

Dans le système réel, cependant, vous configurez l'ensemble du conteneur au niveau le plus élevé du logiciel. Dans une application Web, par exemple, vous configurez le conteneur, en câblant toutes les interfaces et les classes d'implémentation, en GlobalApplication.Application_Start.

0

Je développe un peu la réponse de Peter.

Dans les applications avec de nombreux types d'entités, il n'est pas rare qu'un automate exige des références à plusieurs référentiels, services, etc. Je trouve fastidieux de passer manuellement toutes ces dépendances dans mon code de test (d'autant plus qu'un test donné peut ne concerner qu'un ou deux d'entre eux). Dans ces scénarios, je préfère l'IOC style set-injection sur l'injection du constructeur. Le modèle que je l'utilise ceci:

public class QuestionsController : ControllerBase 
{ 
    private IQuestionsRepository Repository 
    { 
     get { return _repo ?? (_repo = IoC.GetInstance<IQuestionsRepository>()); } 
     set { _repo = value; } 
    } 
    private IQuestionsRepository _repo; 

    // Don't need anything fancy in the ctor 
    public QuestionsController() 
    { 
    } 
} 

Remplacer IoC.GetInstance<> avec tout votre syntaxe cadre particulier du CIO utilise. En production, rien n'invoquera le setter de la propriété, donc la première fois que le getter est appelé, le contrôleur appellera votre framework IOC, récupérera une instance et la stockera.

Dans le test, il vous suffit d'appeler le poseur avant d'appeler toutes les méthodes de contrôleur:

var controller = new QuestionsController { 
    Repository = MakeANewMockHoweverYouNormallyDo(...); 
} 

Les avantages de cette approche, mon humble avis:

  1. prend encore l'avantage de la COI dans la production.
  2. Plus facile à construire manuellement vos contrôleurs pendant les tests. Vous avez seulement besoin d'initialiser les dépendances que votre test utilisera réellement.
  3. Possibilité de créer des configurations IOC spécifiques au test si vous ne souhaitez pas configurer manuellement les dépendances communes.