2010-06-28 4 views
1

J'ai une Foo classe qui utilise une autre barre de classe pour faire des choses. J'essaie de faire du développement piloté par les tests, et donc j'écris des tests unitaires pour Foo pour m'assurer qu'il appelle les méthodes appropriées sur Bar, et pour cela j'utilise l'injection de dépendances et le moqueur Bar (en utilisant Rhino Mocks).construction/d'initialisation de l'objet de test Unité

E.g. (En C#):

class Foo 
{ 
    private IBar bar= null; 

    public Foo(IBar bar) 
    { 
    this.bar= bar; 
    } 

    public void DoSomething() 
    { 
    bar.DoIt(); 
    } 
} 

class FooTests 
{ 
    ... 
    void DoSomethingCallsBarsDoItMethod() 
    { 
    IBar mockBar= MockRepository.GenerateMock<IBar>(); 
    mockBar.Expect(b=>b.DoIt());  
    Foo foo= new Foo(mockBar); 
    foo.DoSomething(); 
    mockBar.VerifyAllExpectations(); 
    } 
} 

Tout cela semble bien, mais je veux vraiment Bar à configurer avec un paramètre particulier, et allait avoir ce paramètre passé via le constructeur de Foo. E.g. (En C#):

public Foo(int x) 
{ 
    this.bar = new Bar(x); 
} 

Je ne sais pas quelle est la meilleure façon de changer cela pour être plus facilement testables. Une des options que je peux penser consiste à déplacer l'initialisation de Bar hors de son constructeur, par ex. J'ai l'impression que cela rend Bar plus difficile à utiliser cependant. Je soupçonne qu'il pourrait y avoir une sorte de solution qui impliquerait l'utilisation d'un conteneur IoC configuré pour injecter Foo avec une imitation IBar et fournirait également un accès à la maquette créée pour la validation des attentes, mais je pense que cela rendrait Foo inutilement complexe . Je suis seulement en utilisant l'injection de dépendance pour me permettre de se moquer des dépendances pour les tests, et donc je ne suis pas en utilisant des conteneurs IoC actuellement, juste construire des dépendances dans un appel constructeur enchaîné des constructeurs par défaut (même si je sais que cela crée un code plus non testé) par exemple

public Foo() : 
    this(new Bar()) 
{ 
} 

Quelqu'un peut-il recommander la meilleure façon de tester la construction/initialisation d'objet dépendant?

Répondre

2

Il me semble que vous laissez une fuite de détails d'implémentation à travers la conception API. IBar ne devrait rien savoir de Foo et de ses exigences.

Barre d'outils avec Constructeur Injection comme vous l'avez fait avec Foo.Vous pouvez maintenant faire partager la même instance en fournissant de l'extérieur:

var x = 42; 
IBar bar = new Bar(x); 
Foo foo = new Foo(bar, x); 

Cela nécessiterait votre constructeur Foo à ressembler à ceci:

public class Foo 
{ 
    private readonly int x; 
    private readonly IBar bar; 

    public Foo(IBar bar, int x) 
    { 
     if (bar == null) 
     { 
      throw new ArgumentNullException("bar"); 
     } 

     this.bar = bar; 
     this.x = x; 
    } 
} 

Cela permet Foo de traiter toute mise en œuvre de IBar sans laisser passer les détails de l'implémentation. Séparer IBar de peut être considéré comme une implémentation du Interface Segregation Principle.

DI terminologie Container, x peut être considéré comme étant Singleton-scope (à ne pas confondre avec le modèle de conception Singleton), car il n'y a qu'une seule instance de x à travers de nombreux composants différents. L'instance est partagé et il est strictement une gestion à vie préoccupation - pas un souci de conception API.

La plupart des conteneurs DI prennent en charge la définition d'un service en tant que singleton, mais un conteneur DI n'est pas requis. Comme le montre l'exemple ci-dessus, vous pouvez également câbler des services partagés à la main.

Dans les cas où vous ne connaissez pas la valeur de x jusqu'à l'exécution, Abstract Factory is the universal solution.

+0

Foo ne se soucie pas de x, sauf que Bar doit être initialisé avec lui. J'ai édité ma question pour clarifier ceci. Merci. – Ergwun

+0

@Ergwun: Si Foo ne se soucie pas de x, il suffit de créer Bar avec x et de l'injecter dans Foo. Si x ne peut pas être connu avant l'exécution, utilisez une Usine abstraite pour créer IBar dans Foo - voir le dernier lien de ma réponse. –

+0

Après avoir essayé pendant un certain temps, j'ai trouvé que l'utilisation de la fabrique de motifs abstraits était extrêmement utile pour ce genre de problème. Merci! – Ergwun

1

cette variable x Définir dans votre cadre d'injection de dépendance, et l'injecter à la fois Foo et Bar.

0

Comment est Foo créé? Cela vient-il d'un conteneur d'inversion de contrôle? Si c'est le cas, vous pouvez utiliser les fonctionnalités de votre conteneur IoC pour configurer ces objets.

Sinon, il suffit de mordre la balle et d'ajouter une méthode Foo.Initializer (paramètre) qui appelle Bar.Initialize (paramètre) ou prend peut-être une instance de Bar.

Je n'ai pas fait des tests unitaires beaucoup plus vous, mais je l'ai trouvé que je ne fais pas les choses plus dans les constructeurs. Cela entrave les essais. Un constructeur prend les dépendances des objets, puis c'est fait.

+0

Foo ne vient pas d'un conteneur IoC pour le moment. La solution d'initialiseurs séparés est celle sur laquelle je me suis penché pour l'instant. Un autre inconvénient à cela consisterait à mettre toutes les autres méthodes publiques en litige avec des assertions et/ou des exceptions pour se prémunir contre l'utilisation d'objets non initialisés, lorsque cela ne serait pas nécessaire si le constructeur pouvait effectuer l'initialisation. Merci. – Ergwun

0

Ceci est plus une question de design. Pourquoi Foo a-t-il besoin de Bar initialisé avec une valeur particulière de x? Potentiellement, les limites de classe ne sont pas définies de manière optimale. Peut-être Bar ne doit pas résumer la valeur de x du tout, il suffit de prendre comme paramètre dans l'appel de méthode:

class Foo 
{ 
    private IBar bar= null; 
    private int x; 

    public Foo(IBar bar, int x) 
    { 
    this.bar= bar; 
    this.x = x; 
    } 

    public void DoSomething() 
    { 
    bar.DoIt(x); 
    } 
} 

Si pour une raison quelconque, vous ne pouvez pas changer la conception que je pense que je voudrais juste avoir une affirmation dans le constructeur:

class Foo 
{ 
    private IBar bar= null; 

    public Foo(IBar bar) 
    { 
    if(bar.X != 42) 
    { 
     throw new ArgumentException("Expected Bar with X=42, but got X="+bar.X); 
    } 
    this.bar= bar; 
    } 

    public void DoSomething() 
    { 
    bar.DoIt(); 
    } 
} 

Cet exemple montre aussi que vous devez avoir des tests d'intégration pour vous assurer que toutes les classes individuelles sont correctement intégrées à l'autre :)

+0

Pour donner un peu plus de contexte, Bar est en fait responsable du séquençage des messages, et selon le protocole existant, le numéro de séquence initial est configurable. Foo.DoSomething() passera réellement des messages et des numéros de séquence, que Bar utilise pour commander des messages avant le traitement. – Ergwun

+0

Mais pourquoi Foo a besoin de connaître le nombre initial? – Grzenio

+0

Grzenio, Foo n'a pas besoin de le savoir, sauf pour le fait que j'ai fait construire Bar avec elle à l'intérieur de Foo. Je pourrais l'avoir passé à Bar en dehors du constructeur, mais j'ai abandonné l'approche de modèle d'usine abstraite suggérée dans une autre réponse. Merci. – Ergwun

0

Je venais de surcharger le constructeur, en avoir un qui accepte x, un autre qui accepte une barre. Cela rend votre objet plus facile à utiliser et plus facile à tester.

//containing class omitted 
public Foo(Bar bar) { 
    this.bar = bar; 
} 

public Foo(int x) { 
    this(new DefaultBar(x)); 
} 

C'est pour cela que la surcharge du constructeur a été inventée.

+0

Je ne vois pas comment l'un ou l'autre de ces constructeurs me permet de tester que Foo initialise correctement Bar. Dans l'ancien Foo n'est pas l'initialisation de Bar, et dans le dernier Bar ne peut pas être moqué. – Ergwun