unit-testing
Afhankelijkheid injectie
Zoeken…
Opmerkingen
Een benadering die kan worden gebruikt voor het schrijven van software is het creëren van afhankelijkheden wanneer deze nodig zijn. Dit is een vrij intuïtieve manier om een programma te schrijven en is de manier waarop de meeste mensen de neiging hebben om les te krijgen, deels omdat het gemakkelijk te volgen is. Een van de problemen met deze aanpak is dat het moeilijk kan zijn om te testen. Overweeg een methode die enige verwerking uitvoert op basis van de huidige datum. De methode kan code bevatten zoals de volgende:
if (DateTime.Now.Date > processDate)
{
// Do some processing
}
De code is direct afhankelijk van de huidige datum. Deze methode kan moeilijk te testen zijn omdat de huidige datum niet gemakkelijk kan worden gemanipuleerd. Een benadering om de code testbaarder te maken, is om de directe verwijzing naar de huidige datum te verwijderen en in plaats daarvan de huidige datum in te voeren (of te injecteren) voor de methode die de verwerking uitvoert. Deze afhankelijkheidsinjectie kan het veel gemakkelijker maken om aspecten van code te testen door testdubbels te gebruiken om de instellingsstap van de eenheidstest te vereenvoudigen.
IOC-systemen
Een ander aspect om te overwegen is de levensduur van afhankelijkheden; in het geval dat de klasse zelf zijn eigen afhankelijkheden creëert (ook bekend als invarianten), is hij dan verantwoordelijk voor het weggooien ervan. Dependency Injection keert dit om (en dit is de reden waarom we een injectiebibliotheek vaak een 'Inversion of Control'-systeem noemen) en betekent dat in plaats van dat de klasse verantwoordelijk is voor het creëren, beheren en opschonen van zijn afhankelijkheden, een externe agent (in dit geval, het IoC-systeem) doet het in plaats daarvan.
Dit maakt het veel eenvoudiger om afhankelijkheden te hebben die worden gedeeld tussen instanties van dezelfde klasse; overweeg bijvoorbeeld een service die gegevens ophaalt van een HTTP-eindpunt voor een klasse om te consumeren. Omdat deze service staatloos is (dwz dat deze geen interne status heeft), hebben we eigenlijk maar één exemplaar van deze service nodig in onze applicatie. Hoewel het mogelijk is (bijvoorbeeld door een statische klasse te gebruiken) om dit handmatig te doen, is het veel eenvoudiger om de klasse te maken en het IoC-systeem te vertellen dat deze moet worden gemaakt als Singleton , waarbij er slechts één instantie van de klasse bestaat.
Een ander voorbeeld zijn databasecontexten die worden gebruikt in een webtoepassing, waarbij een nieuwe context vereist is per aanvraag (of thread) en niet per instantie van een controller; hierdoor kan de context worden geïnjecteerd in elke laag die door die thread wordt uitgevoerd, zonder handmatig te moeten worden doorgegeven.
Hierdoor hoeven de consumerende klassen de afhankelijkheden niet te beheren.
Constructor injectie
Constructorinjectie is de veiligste manier om afhankelijkheden te injecteren waarvan een hele klasse afhankelijk is. Dergelijke afhankelijkheden worden vaak invarianten genoemd , omdat een instantie van de klasse niet kan worden gemaakt zonder ze te leveren. Door te eisen dat de afhankelijkheid bij de constructie wordt geïnjecteerd, wordt gegarandeerd dat een object niet in een inconsistente staat kan worden gemaakt.
Overweeg een klasse die in foutcondities naar een logbestand moet schrijven. Het is afhankelijk van een ILogger
, die indien nodig kan worden geïnjecteerd en gebruikt.
public class RecordProcessor
{
readonly private ILogger _logger;
public RecordProcessor(ILogger logger)
{
_logger = logger;
}
public void DoSomeProcessing() {
// ...
_logger.Log("Complete");
}
}
Soms merk je tijdens het schrijven van tests op dat de constructor meer afhankelijkheden vereist dan nodig is voor een case die wordt getest. Hoe meer van dergelijke tests je hebt, des te waarschijnlijker is het dat je klas Single Responsibility Principle (SRP) breekt. Daarom is het geen goede gewoonte om het standaardgedrag te definiëren voor alle mocking van geïnjecteerde afhankelijkheden in de initialisatiefase van de testklasse, omdat dit het potentiële waarschuwingssignaal kan maskeren.
De unittest hiervoor zou er als volgt uitzien:
[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());
}
Eigendom injectie
Met eigenschapsinjectie kunnen klassenafhankelijkheden worden bijgewerkt nadat deze is gemaakt. Dit kan handig zijn als u het maken van objecten wilt vereenvoudigen, maar toch toestaat dat de afhankelijkheden worden overschreven door uw tests met testdubbels.
Overweeg een klasse die in een foutconditie naar een logboekbestand moet schrijven. De klasse weet een standaard Logger
te construeren, maar laat het overschrijven door middel van injectie van eigenschappen. Het is echter vermeldenswaard dat u op deze manier met eigenschapinjectie deze klasse nauw ILogger
aan een exacte implementatie van ILogger
die ConcreteLogger
in dit gegeven voorbeeld. Een mogelijke oplossing kan een fabriek zijn die de benodigde ILogger-implementatie retourneert.
public class RecordProcessor
{
public RecordProcessor()
{
Logger = new ConcreteLogger();
}
public ILogger Logger { get; set; }
public void DoSomeProcessing()
{
// ...
_logger.Log("Complete");
}
}
In de meeste gevallen heeft Constructor Injection de voorkeur boven Property Injection omdat het betere garanties biedt over de staat van het object onmiddellijk na de constructie.
Methode injectie
Methode-injectie is een fijne manier om afhankelijkheden in de verwerking te injecteren. Overweeg een methode die enige verwerking uitvoert op basis van de huidige datum. De huidige datum is moeilijk te veranderen van een test, dus het is veel gemakkelijker om een datum door te geven aan de methode die u wilt testen.
public void ProcessRecords(DateTime currentDate)
{
foreach(var record in _records)
{
if (currentDate.Date > record.ProcessDate)
{
// Do some processing
}
}
}
Containers / DI Frameworks
Terwijl afhankelijkheden uit uw code worden geëxtraheerd zodat ze kunnen worden geïnjecteerd, wordt uw code gemakkelijker te testen, maar wordt het probleem hoger in de hiërarchie en kan het ook resulteren in moeilijk te construeren objecten. Verschillende frameworks voor afhankelijkheidsinjectie / Inversion of Control Containers zijn geschreven om dit probleem te verhelpen. Hiermee kunnen typetoewijzingen worden geregistreerd. Deze registraties worden vervolgens gebruikt om afhankelijkheden op te lossen wanneer de container wordt gevraagd om een object te construeren.
Overweeg deze klassen:
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)
{
// ...
}
}
Om SomeProcessor
te bouwen, zijn zowel een exemplaar van ILogger
als SimpleClass
vereist. Een container als Unity helpt dit proces te automatiseren.
Eerst moet de container worden gebouwd en vervolgens worden toewijzingen ermee geregistreerd. Dit wordt meestal slechts eenmaal binnen een toepassing gedaan. Het gebied van het systeem waar dit gebeurt, wordt gewoonlijk de compositiewortel genoemd
// 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());
Nadat de container is geconfigureerd, kan deze worden gebruikt om objecten te maken en afhankelijkheden automatisch op te lossen:
var processor = container.Resolve<SomeProcessor>();