unit-testing
Injection de dépendance
Recherche…
Remarques
Une approche qui peut être prise pour écrire un logiciel consiste à créer des dépendances au besoin. C'est une manière assez intuitive d'écrire un programme et c'est la manière dont la plupart des gens auront tendance à apprendre, en partie parce que c'est facile à suivre. L'un des problèmes de cette approche est qu'il peut être difficile à tester. Considérons une méthode qui effectue un traitement en fonction de la date actuelle. La méthode peut contenir du code comme celui-ci:
if (DateTime.Now.Date > processDate)
{
// Do some processing
}
Le code a une dépendance directe à la date actuelle. Cette méthode peut être difficile à tester car la date actuelle ne peut pas être facilement manipulée. Une méthode pour rendre le code plus vérifiable consiste à supprimer la référence directe à la date actuelle et à fournir (ou injecter) la date actuelle à la méthode qui effectue le traitement. Cette injection de dépendance permet de tester plus facilement des aspects du code en utilisant des doubles de test pour simplifier l’étape d’installation du test unitaire.
Systèmes IOC
Un autre aspect à considérer est la durée de vie des dépendances; dans le cas où la classe elle-même crée ses propres dépendances (également appelées invariants), elle est alors responsable de leur disposition. Dependency Injection inverse (et c'est pourquoi nous faisons souvent référence à une bibliothèque d'injection comme un système "Inversion of Control") et que, au lieu que la classe soit responsable de la création, la gestion et le nettoyage de ses dépendances, un agent externe cas, le système IoC) le fait à la place.
Cela rend beaucoup plus simple d'avoir des dépendances partagées entre des instances de la même classe; Par exemple, considérez un service qui récupère des données à partir d'un point de terminaison HTTP pour une classe à consommer. Comme ce service est sans état (c'est-à-dire qu'il n'a pas d'état interne), nous n'avons besoin que d'une seule instance de ce service dans notre application. Bien qu'il soit possible (par exemple en utilisant une classe statique) de le faire manuellement, il est beaucoup plus simple de créer la classe et d'indiquer au système IoC qu'elle doit être créée en tant que Singleton .
Un autre exemple serait les contextes de base de données utilisés dans une application Web, un nouveau contexte étant requis par demande (ou thread) et non par instance de contrôleur; Cela permet d'injecter le contexte dans chaque couche exécutée par ce thread, sans avoir à être passé manuellement.
Cela libère les classes consommatrices de la gestion des dépendances.
Constructeur Injection
L'injection de constructeur est le moyen le plus sûr d'injecter des dépendances dont dépend toute une classe. De telles dépendances sont souvent appelées invariants , car il est impossible de créer une instance de la classe sans les fournir. En exigeant que la dépendance soit injectée lors de la construction, il est garanti qu'un objet ne peut pas être créé dans un état incohérent.
Considérons une classe qui doit écrire dans un fichier journal dans des conditions d'erreur. Il a une dépendance à un ILogger
, qui peut être injecté et utilisé si nécessaire.
public class RecordProcessor
{
readonly private ILogger _logger;
public RecordProcessor(ILogger logger)
{
_logger = logger;
}
public void DoSomeProcessing() {
// ...
_logger.Log("Complete");
}
}
Parfois, lors de l'écriture de tests, vous pouvez noter que le constructeur nécessite plus de dépendances que ce qui est réellement nécessaire pour un cas testé. Plus vous avez de tests de ce type, plus il est probable que votre classe casse le principe de responsabilité unique (SRP). C'est pourquoi il n'est pas très judicieux de définir le comportement par défaut de toutes les dépendances injectées lors de la phase d'initialisation de la classe de test, car il peut masquer le signal d'avertissement potentiel.
Le plus important pour cela serait le suivant:
[Test]
public void RecordProcessor_DependencyInjectionExample()
{
ILogger logger = new FakeLoggerImpl(); //or create a mock by a mocking Framework
var sut = new RecordProcessor(logger); //initialize with fake impl in testcode
Assert.IsTrue(logger.HasCalledExpectedMethod());
}
Injection de propriété
L'injection de propriété permet aux dépendances d'une classe d'être mises à jour après sa création. Cela peut être utile si vous souhaitez simplifier la création d’objets, tout en permettant à vos tests de remplacer les dépendances par des tests doubles.
Considérons une classe qui doit écrire dans un fichier journal dans une condition d'erreur. La classe sait comment construire un Logger
par défaut, mais permet de la remplacer par une injection de propriété. Cependant, cela vaut la peine de noter que l'utilisation de l'injection de propriété de cette façon vous permet de coupler étroitement cette classe avec une implémentation exacte de ILogger
qui est ConcreteLogger
dans cet exemple. Une solution de contournement possible pourrait être une fabrique qui renvoie l'implémentation ILogger nécessaire.
public class RecordProcessor
{
public RecordProcessor()
{
Logger = new ConcreteLogger();
}
public ILogger Logger { get; set; }
public void DoSomeProcessing()
{
// ...
_logger.Log("Complete");
}
}
Dans la plupart des cas, l'injection de constructeur est préférable à l'injection de propriété car elle fournit de meilleures garanties sur l'état de l'objet immédiatement après sa construction.
Méthode injection
L'injection de méthode est une manière fine d'injecter des dépendances dans le traitement. Considérons une méthode qui effectue un traitement en fonction de la date actuelle. La date actuelle est difficile à modifier depuis un test, il est donc beaucoup plus facile de transmettre une date à la méthode que vous souhaitez tester.
public void ProcessRecords(DateTime currentDate)
{
foreach(var record in _records)
{
if (currentDate.Date > record.ProcessDate)
{
// Do some processing
}
}
}
Cadres Conteneurs / DI
Tandis que l'extraction des dépendances de votre code pour qu'elles puissent être injectées rend votre code plus facile à tester, il pousse le problème plus haut dans la hiérarchie et peut également générer des objets difficiles à construire. Divers cadres d'injection de dépendance / Inversion des conteneurs de contrôle ont été écrits pour aider à résoudre ce problème. Celles-ci permettent d’enregistrer les mappages de types. Ces enregistrements sont ensuite utilisés pour résoudre les dépendances lorsque le conteneur est invité à créer un objet.
Considérons ces classes:
public interface ILogger {
void Log(string message);
}
public class ConcreteLogger : ILogger
{
public ConcreteLogger()
{
// ...
}
public void Log(string message)
{
// ...
}
}
public class SimpleClass
{
public SimpleClass()
{
// ...
}
}
public class SomeProcessor
{
public SomeProcessor(ILogger logger, SimpleClass simpleClass)
{
// ...
}
}
Pour construire SomeProcessor
, une instance de ILogger
et de SimpleClass
est requise. Un conteneur comme Unity aide à automatiser ce processus.
D'abord, le conteneur doit être construit et les correspondances sont enregistrées avec lui. Cela se fait généralement une seule fois dans une application. La zone du système où cela se produit est communément appelée racine de composition
// Register the container
var container = new UnityContainer();
// Register a type mapping. This allows a `SimpleClass` instance
// to be constructed whenever it is required.
container.RegisterType<SimpleClass, SimpleClass>();
// Register an instance. This will use this instance of `ConcreteLogger`
// Whenever an `ILogger` is required.
container.RegisterInstance<ILogger>(new ConcreteLogger());
Une fois le conteneur configuré, il peut être utilisé pour créer des objets, en résolvant automatiquement les dépendances selon les besoins:
var processor = container.Resolve<SomeProcessor>();