web-dev-qa-db-fra.com

L'inversion de dépendance élargit l'API, entraîne des tests inutiles

Cette question m'a dérangé pendant quelques jours et on a l'impression que plusieurs pratiques se contredisent.

exemple

Itération 1

public class FooDao : IFooDao
{
    private IFooConnection fooConnection;
    private IBarConnection barConnection;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooConnection = fooConnection;
        this.barConnection = barConnection;
    }

    public Foo GetFoo(int id)
    {
        Foo foo = fooConection.get(id);
        Bar bar = barConnection.get(foo);
        foo.bar = bar;
        return foo;
    }
}

Maintenant, lorsque vous testez cela, je ferais une simulation d'ifooConnection et d'ibarconnection et utilisez l'injection de dépendance (DI) lors de l'instanciation de Foodao.

Je peux changer la mise en œuvre sans changer la fonctionnalité.

Itération 2

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Maintenant, je n'écrirai pas ce constructeur, mais imaginez que cela fait la même chose Foodao auparavant. Ceci est juste un refactoring, donc évidemment, cela ne change pas la fonctionnalité, et donc, mon test passe toujours.

Ifoobuilder est interne, car il n'existe que de travailler pour la bibliothèque, c'est-à-dire que cela ne fait pas partie de l'API.

Le seul problème est que je ne suis plus conforme à l'inversion de dépendance. Si je réécrit cela pour résoudre ce problème, cela pourrait ressembler à ceci.

Itération

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Cela devrait le faire. J'ai changé le constructeur. Mon test doit donc changer pour soutenir cela (ou l'inverse), ce n'est cependant pas ma question cependant.

Pour que cela fonctionne avec Di, Foobuilder et Ifoobuilder, doivent être publics. Cela signifie que je devrais écrire un test pour Foobuilder, car il est soudainement devenu une partie de l'API de mon bibliothèque. Mon problème est que les clients de la bibliothèque ne devraient l'utiliser que sur ma conception de l'API prévue, si ioodao et mes tests sert de clients. Si je ne suivez pas l'inversion de dépendance, mes tests et mes API sont plus propres.

En d'autres termes, tout ce qui m'importe que le client, ou comme le test, consiste à obtenir le bon foo, et non comment il est construit.

Solutions

  • Devrais-je simplement m'occuper, écrivez les tests de Foobuilder, même s'il n'est public que s'il vous plaît di? - Prend en charge l'itération 3

  • Devrais-je me rendre compte que l'expansion de l'API est une inverversion d'inconvénération de la dépendance et d'être très claire sur la raison pour laquelle j'ai choisi de ne pas me conformer ici? - Prend en charge l'itération 2

  • Dois-je mettre trop d'importance à avoir une API propre? - Prend en charge l'itération 3

EDIT: Je veux préciser, que mon problème est non "Comment tester des internes?", Plutôt quelque chose comme "puis-je le garder interne, et je suis toujours conforme à la trempette. ? ".

8
Chris Wohlert

Je n'aime pas particulièrement cette notion que vous devez exposer des méthodes privées (quels que soient les moyens) afin d'effectuer des tests appropriés. Je suggère également que votre client face à l'API soit non la même chose que la méthode publique de votre objet. Voici votre interface publique:

interface IFooDao {
   public Foo GetFoo(int id);
}

et il n'y a aucune mention de votre FooBuilder

Dans votre question originale, je prendrais la troisième route en utilisant votre FooBuilder. Je vais peut-être tester votre FooDao à l'aide d'un Mock, concentrant ainsi sur votre fonctionnalité FooDao (vous pouvez toujours injecter un véritable constructeur si c'est plus facile) et j'aurais séparément Tests autour de votre FooBuilder.

(Je suis surpris que la moqueur n'a pas été mentionnée dans cette question/réponse - elle va de pair avec des solutions à motifs DI/IOC)

Ré. votre commentaire:

Comme indiqué, le constructeur ne fournit aucune nouvelle fonctionnalité. Je n'ai donc pas besoin de le tester, cependant, la trempette en fait partie de l'API, ce qui me force à le tester.

Je ne pense pas que cela élargit l'API que vous présentez à vos clients. Vous pouvez limiter l'API que vos clients utilisent via des interfaces (si vous le souhaitez). Je suggérerais également que vos tests ne devraient pas vienner d'entourer vos composants au public. Ils devraient embrasser toutes les classes (implémentations) qui soutiennent l'API en dessous. par exemple. Voici le REST API pour le système, je travaille actuellement sur:

(en pseudo-code)

void doSomeCalculation(json : CalculationRequestObject);

J'ai une méthode de l'API et 1 000 tests - la grande majorité desquels sont autour des objets qui soutiennent cela.

0
Brian Agnew

Pourquoi voulez-vous suivre la DI dans ce cas s'il n'est pas à des fins de test? Si non seulement votre API publique, mais aussi vos tests sont vraiment plus propres sans un paramètre IFooBuilder (comme vous l'avez écrit), il n'a aucun sens d'introduire une telle classe. DI n'est pas une fin en soi, cela devrait vous aider à rendre votre code plus testable. Si vous ne souhaitez pas écrire des tests automatisés pour ce niveau d'abstraction particulier, n'appliquez pas DI à ce niveau.

Cependant, supposons qu'un moment où vous souhaitez appliquer di pour permettre de créer des tests d'unité pour le constructeur FooDao isolant sans le processus de construction, mais je ne veux toujours pas de constructeur FooDao(IFooBuilder fooBuilder) Constructeur dans votre API publique, seulement un constructeur FooDao(IFooConnection fooConnection, IBarConnection barConnection). Fournissez ensuite les deux, le premier comme "interne", ce dernier comme "public":

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    // only for testing purposes!    
    internal FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    // the public API
    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }    

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Maintenant, vous pouvez garder FooBuilder et IFooBuilder interne et appliquer le InternalsVisibleTo pour les rendre accessibles par des tests unitaires.

0
Doc Brown