unit-testing
Abhängigkeitsspritze
Suche…
Bemerkungen
Ein Ansatz, der zum Schreiben von Software verwendet werden kann, besteht darin, Abhängigkeiten so zu erstellen, wie sie benötigt werden. Dies ist eine sehr intuitive Art, ein Programm zu schreiben, und die meisten Leute werden dazu neigen, unterrichtet zu werden, auch weil sie leicht zu verstehen sind. Eines der Probleme bei diesem Ansatz ist, dass er schwer zu testen sein kann. Stellen Sie sich eine Methode vor, die einige Verarbeitungsschritte basierend auf dem aktuellen Datum ausführt. Die Methode kann folgenden Code enthalten:
if (DateTime.Now.Date > processDate)
{
// Do some processing
}
Der Code hängt direkt vom aktuellen Datum ab. Diese Methode kann schwer zu testen sein, da das aktuelle Datum nicht einfach manipuliert werden kann. Eine Möglichkeit, den Code besser testbar zu machen, besteht darin, den direkten Verweis auf das aktuelle Datum zu entfernen und stattdessen das aktuelle Datum an die Methode zu übergeben (oder zu injizieren), die die Verarbeitung durchführt. Diese Abhängigkeitsinjektion kann das Testen von Code-Aspekten durch Verwendung von Test-Doubles vereinfachen, um den Einrichtungsschritt des Komponententests zu vereinfachen.
IOC-Systeme
Ein weiterer zu berücksichtigender Aspekt ist die Lebensdauer von Abhängigkeiten. Wenn die Klasse selbst ihre eigenen Abhängigkeiten (auch als Invarianten bezeichnet) erstellt, ist sie für deren Beseitigung verantwortlich. Abhängigkeitsinjektion invertiert dies (weshalb wir eine Injektionsbibliothek häufig als "Inversion of Control" -System bezeichnen) und bedeutet, dass anstelle der Klasse, die für das Erstellen, Verwalten und Bereinigen ihrer Abhängigkeiten verantwortlich ist, ein externer Agent (in diesem Fall) In diesem Fall übernimmt das IoC-System dies.
Dies macht es viel einfacher, Abhängigkeiten zu haben, die von Instanzen derselben Klasse gemeinsam genutzt werden. Stellen Sie sich beispielsweise einen Dienst vor, der Daten von einem HTTP-Endpunkt abruft, damit eine Klasse sie konsumieren kann. Da dieser Dienst zustandslos ist (dh keinen internen Status hat), benötigen wir in der gesamten Anwendung nur eine einzige Instanz dieses Dienstes. Es ist zwar möglich (z. B. durch Verwendung einer statischen Klasse), dies manuell durchzuführen, es ist jedoch viel einfacher, die Klasse zu erstellen und dem IoC-System mitzuteilen, dass es als Singleton erstellt werden soll , wobei nur eine Instanz der Klasse vorhanden ist.
Ein anderes Beispiel wären Datenbankkontexte, die in einer Webanwendung verwendet werden, wobei ein neuer Kontext pro Anforderung (oder Thread) und nicht pro Instanz eines Controllers erforderlich ist. Dadurch kann der Kontext in jede von diesem Thread ausgeführte Ebene eingefügt werden, ohne dass manuell weitergegeben werden muss.
Dies befreit die konsumierenden Klassen von der Verwaltung der Abhängigkeiten.
Konstruktorinjektion
Konstruktorinjektion ist der sicherste Weg, Abhängigkeiten zu injizieren, von denen eine ganze Klasse abhängt. Solche Abhängigkeiten werden oft als Invarianten bezeichnet , da eine Instanz der Klasse nicht erstellt werden kann, ohne sie anzugeben. Durch die Anforderung, dass die Abhängigkeit bei der Konstruktion eingefügt wird, ist sichergestellt, dass ein Objekt nicht in einem inkonsistenten Zustand erstellt werden kann.
Stellen Sie sich eine Klasse vor, die unter Fehlerbedingungen in eine Protokolldatei schreiben muss. Es hängt von einem ILogger
, der injiziert und bei Bedarf verwendet werden kann.
public class RecordProcessor
{
readonly private ILogger _logger;
public RecordProcessor(ILogger logger)
{
_logger = logger;
}
public void DoSomeProcessing() {
// ...
_logger.Log("Complete");
}
}
Beim Schreiben von Tests stellen Sie möglicherweise fest, dass der Konstruktor mehr Abhängigkeiten erfordert, als für einen getesteten Fall tatsächlich erforderlich sind. Je mehr solcher Tests Sie durchführen, desto wahrscheinlicher ist es, dass Ihre Klasse gegen das Single Responsibility Principle (SRP) verstößt. Aus diesem Grund ist es nicht empfehlenswert, das Standardverhalten für alle Mocks injizierter Abhängigkeiten in der Testklasseninitialisierungsphase zu definieren, da dies das potenzielle Warnsignal maskieren kann.
Das Einzige, was dafür aussieht, würde wie folgt aussehen:
[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());
}
Immobilieninjektion
Durch die Eigenschaftsinjektion können Klassenabhängigkeiten nach der Erstellung aktualisiert werden. Dies kann nützlich sein, wenn Sie die Objekterstellung vereinfachen möchten, aber dennoch zulassen, dass die Abhängigkeiten von Ihren Tests mit Testverdopplungen überschrieben werden.
Stellen Sie sich eine Klasse vor, die unter einer Fehlerbedingung in eine Protokolldatei geschrieben werden muss. Die Klasse weiß, wie ein Standard- Logger
kann jedoch durch die Eigenschaftsinjektion überschrieben werden. Es lohnt sich jedoch zu beachten, dass durch die Verwendung der Eigenschaftsinjektion auf diese Weise diese Klasse eng mit einer genauen Implementierung von ILogger
wird, in diesem Beispiel ConcreteLogger
. Eine mögliche Problemumgehung könnte eine Fabrik sein, die die erforderliche ILogger-Implementierung zurückgibt.
public class RecordProcessor
{
public RecordProcessor()
{
Logger = new ConcreteLogger();
}
public ILogger Logger { get; set; }
public void DoSomeProcessing()
{
// ...
_logger.Log("Complete");
}
}
In den meisten Fällen ist Konstruktorinjektion der Eigenschaftsinjektion vorzuziehen, da dadurch der Zustand des Objekts unmittelbar nach seiner Konstruktion besser gewährleistet werden kann.
Methode Injection
Die Methodeninjektion ist eine feinkörnige Methode, um Abhängigkeiten in die Verarbeitung einzubringen. Stellen Sie sich eine Methode vor, die einige Verarbeitungsschritte basierend auf dem aktuellen Datum ausführt. Das aktuelle Datum kann von einem Test nur schwer geändert werden. Daher ist es viel einfacher, ein Datum in die Methode zu übernehmen, die Sie testen möchten.
public void ProcessRecords(DateTime currentDate)
{
foreach(var record in _records)
{
if (currentDate.Date > record.ProcessDate)
{
// Do some processing
}
}
}
Container / DI-Frameworks
Durch das Extrahieren von Abhängigkeiten aus Ihrem Code, sodass sie injiziert werden können, ist der Code einfacher zu testen, doch das Problem wird in der Hierarchie weiter nach oben gerückt und kann dazu führen, dass Objekte schwer zu erstellen sind. Verschiedene Frameworks zur Abhängigkeitsinjektion / Inversion von Kontrollcontainern wurden geschrieben, um dieses Problem zu lösen. Diese ermöglichen die Registrierung von Typzuordnungen. Diese Registrierungen werden dann verwendet, um Abhängigkeiten aufzulösen, wenn der Container aufgefordert wird, ein Objekt zu erstellen.
Betrachten Sie diese 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)
{
// ...
}
}
Um SomeProcessor
zu SomeProcessor
, sind sowohl eine Instanz von ILogger
als auch SimpleClass
erforderlich. Ein Container wie Unity hilft, diesen Prozess zu automatisieren.
Zuerst muss der Container erstellt werden und dann werden Mappings damit registriert. Dies erfolgt normalerweise nur einmal innerhalb einer Anwendung. Der Bereich des Systems, in dem dies auftritt, wird im Allgemeinen als Kompositionswurzel bezeichnet
// 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());
Nach der Konfiguration des Containers können Objekte erstellt und Abhängigkeiten automatisch aufgelöst werden:
var processor = container.Resolve<SomeProcessor>();