Dependency Injection in Unittests
In unterschiedlichen Szenarien sind oft unterschiedliche Implementierungen einer Schnittstelle erforderlich - gerade hierfür werden ja Schnittstellen verwendet: Zum Test einer Klasse, die beispielsweise einen Service verwendet, ist es wünschenswert, ein Testdouble (Mock, Stub, Fake, Spy o.ä.) zu verwenden. In einem anderen Ausführungskontext dagegen ist der Einsatz der echten Implementierung erforderlich.
Um nicht in diesen unterschiedlichen Ausführungskontexten manuell die Abhängigkeit zu injizieren oder unterschiedliche Anbindungen manuell zu implementieren, soll ein flexibler Mechanismus verwendet werden, der auch konfigurierbar ist. Dieses Muster beschreibt die Verwendung von DependencyInjection in Unittests.
Design
Für Unittests ist die manuelle Injektion der Testdoubles notwendig.
Implementierung
In diesem Beispiel ist eine Testklasse zu sehen (siehe Testklassen für Unittests erstellen). Das Fixture richtet die Umgebung ein und liefert das zu testende Objekt zurück. Dieser Test soll sicherstellen, dass beim Aufruf der LeiheAus()
-Methode des Controllers die Update()
-Methode des im Controller verwendeten Repositorys aufgerufen wird. Hier wird ein Mock und Stub als Testdouble verwendet.
internal sealed partial class BuecherAusleihenControllerTest { [Test] public void LeiheAus_WhereBenutzerkontoHasNoAusleihen_ShouldSaveBenutzerkonto() { BuecherAusleihenController testObject = fixture.CreateTestObject(); fixture.Mocks.BenutzerkontoRepositoryMock.Setup(x => x.QueryBy(fixture.Data.BenutzerSid)) .Returns(new Benutzerkonto(Guid.NewGuid(), fixture.Data.BenutzerName, fixture.Data.BenutzerSid)); // Stub-Verhalten testObject.LeiheAus(fixture.Data.BenutzerSid, fixture.Data.BuecherIds); fixture.Mocks.BenutzerkontoRepositoryMock.Verify(x => x.Update(It.IsAny<Benutzerkonto>())); // Mock-Verhalten } }
Im Fixture (siehe Fixture für Testklasse erstellen) wird ein Testdouble erzeugt und konfiguriert. Danach wird es per manueller Constructor Injection an das zu testende Objekt übergeben.
internal sealed partial class BuecherAusleihenControllerTest : IDisposable { // ... private sealed class Mocks { // ... public Mock<IBuchRepository> BuchRepositoryMock { get; } = new Mock<IBuchRepository>(); public BuecherAusleihenController CreateTestObject() { return new BuecherAusleihenController( BenutzerkontoRepositoryMock.Object, BuchRepositoryMock.Object, BuecherAusgeliehenEventServiceGatewayMock.Object); } } private sealed class TestData { public string BenutzerSid { get; } = "cals_super_sid"; public string BenutzerName { get; } = "carstens konto"; // ... } private sealed class Fixture : IDisposable { public Mocks Mocks { get; } = new Mocks(); // ... public TestData Data { get; } = new TestData(); public void Dispose() { } public BuecherAusleihenController CreateTestObject() { return Mocks.CreateTestObject(); } } }
Stärken und Schwächen
Stärken
- Es werden die Testdoubles (Mocks, Stubs, etc.) direkt injiziert, was einfacher und verständlicher ist
- Der DI-Container wird nicht direkt sichtbar im Produktivcode verwendet, was eine bessere Trennung von Technik und Fachlichkeit darstellt
- Keine
Create()
-Methoden und somit weniger Code - Keine
Reset()
-Methode für Unittests notwendig - Anhand der Anzahl der Übergabeargumente im Konstruktor kann erkannt werden, dass zu viele Abhängigkeiten vorliegen, was ein schlechtes Design impliziert
- Es wird ein Schnittstellen-orientiertes Design gefördert
Schwächen
- Das Zusammenstellen für Unittests, die z.T. integrativ sind, ist schwer
- Schlechtere Codenavigierbarkeit