Work in Progress
Diese Seite ist aktuell im Review! Die Seite wurde noch nicht qualitätsgesichert und kann Fehler enthalten.
Die verlinkten Seiten sind ggf. nur für Schleupen-Mitarbeiter sichtbar.

ComponentTest für Services

Um implementierte Funktionalität zu prüfen, wird das Gros an Tests als Unittests implementiert (vgl. Teststrategie), da diese zum einen performanter und zum anderen einfacher implementierbar sind. Um auch das Zusammenspiel der einzelnen Klassen sicherzustellen, reicht dies natürlich nicht - hier kommen die ComponentTests zum Zuge. Das Ziel eines ComponentTests ist es, die Funktionalität der Komponente als BlackBox-Test so isoliert wie möglichst ohne Abhängigkeit zu externen Resourcen sicherzustellen. Der Zugriff auf die Datenbank mit dem Schema der Komponente ist als einzige externe Abhängigkeit erlaubt, so dass eine geringe Ausführungszeit und die Ausführung im CI-Build erreicht wird.

Wichtig für den Test ist, dass das Zusammenspiel getestet wird und nicht Details der Programmierung.

Design

Grundlegend für die Implementierung des ComponentTests für Services ist, dass die Ausführung des Tests und der Service-Aufruf in unterschiedlichen App-Domains erfolgen, so dass die Transaktion zum Anlegen der Daten nicht dieselbe ist, wie die evtl. im Service verwendete. Die Ursache ist hier, dass eine Umgebung verwendet wird, die ein abgespeckter aber vollwertiger ServiceHost ist und somit den vollständigen WCF-Stack umfasst. Daraus folgt, dass der Test im Arrange die Daten committen muss, so dass diese für den zu testenden Service sichtbar sind. Entsprechend müssen nach dem Test die Testdaten und die neu erzeugten Daten wieder gelöscht werden.

Die beteiligten Klassen sind hierbei die Testklasse selber, die die Tests beinhaltet sowie das Testspezifische Fixture. Die Struktur ist somit identisch zu der in Testklassen für Unittests beschriebenen mit der Erweiterung, dass das sogenannte ServiceTestFixture zur Bereitstellung eines ServiceHosts verwendet wird.

Folgende grobe Schritte werden bei der Ausführung durchgeführt:

  1. Arrange
    • Persistierung der für den Test notwendigen Daten
    • Starten eines ServiceHosts mit gemockten Abhängigkeiten zur CS 3.0-Infrastruktur
  2. Act
    • Aufruf des zu testenden Services
  3. Assert
    • Validierung des Resultats
    • Herunterfahren des ServiceHosts
    • Löschen der für den Test notwendigen sowie neu erzeugten Daten

Implementierung

Das Konzept wird anhand eines einfachen Beispiels erläutert.

namespace Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihen.ActivityService.V3_3;

internal sealed partial class BuecherAusleihenActivityServiceTest
{
    [Test]
    public void Ausleihen_WhereBuchExists_ShouldReturnAusleiheWithThatBuch()
    {
        // Arrange
        fixture.CreatePersistedBuchSample();
        fixture.CreatePersistedBenutzerkontoSample();
        fixture.WithOpenedServiceHost(
            client =>
            {
                AusleihenRequest ausleihenRequest = ...

                // Act
                AusleihenResponse ausleihenResponse = client.Ausleihen(ausleihenRequest);

                // Assert
                Assert.That(ausleihenResponse, Is.Not.Null);
                Assert.That(ausleihenResponse.AusleiheListe.Count, Is.EqualTo(1));
            });
    }
}

Das Fixture ServiceTestFixture<IBuecherAusleihenActivityService, BuecherAusleihenActivityService> stellt einen Service-Host bereit. Die Klasse TestData kapselt die Fachklassen-spezifischen Fixtures, welche Testdaten erzeugen und persistieren können.

namespace Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihen.ActivityService.V3_3;

internal sealed partial class BuecherAusleihenActivityServiceTest : IDisposable
{
    private Fixture fixture;

    [SetUp]
    public void Setup()
    {
        fixture = new Fixture();
    }

    [TearDown]
    public void Dispose()
    {
        fixture?.Dispose();
        fixture = null;
    }

    private sealed class TestData
    {
        private readonly BuchFixture buchFixture = new BuchFixture();
        //...                        
        public Buch BuchSample { get; private set; }
        // ...
        public void CreateBuchSample()
        {
            BuchSample = buchFixture
            .NewBuch("DDD")
            .Build();
        }
        // ...
    }

    private sealed class Mocks
    {
        public Mock<IBuecherAusgeliehenEventServiceClient> BuecherAusgeliehenEventServiceClientStub { get; }
            = new Mock<IBuecherAusgeliehenEventServiceClient>();
        // ...
    }

    private sealed class Fixture : ServiceTestFixture<IBuecherAusleihenActivityService, BuecherAusleihenActivityService>
    {
        private readonly BuchPersistenceFixture buchPersistenceFixture = new BuchPersistenceFixture();

        public Fixture()
        : base(new TestServiceHostFactoryConfigurator())
        {
            RegisterInstance(Mocks.BuecherAusgeliehenEventServiceClientStub.Object);
            // ...
        }

        public TestData TestData { get; } = new TestData();

        public Mocks Mocks { get; } = new Mocks();

        public void WithOpenedServiceHost(Action<IBuecherAusleihenActivityServiceClient> action)
        {
            Mocks.BuecherAusgeliehenEventServiceClientStub
                .Setup(x => x.RaisedAsync(It.IsAny<RaisedNotification>()))
                .Returns(Task.FromResult(new RaisedResponse()));
            // ...
            base.WithOpenedServiceHost(action);
        }

        public string CreateSessionToken()
        {
            return "Wird nicht gebraucht da die Verbindung über die app.config gezogen wird. "
                + "Dies erfolgt über den TestServiceHostFactoryConfigurator.";
        }

        public void CreatePersistedBenutzerkontoSample()
        {
            TestData.CreateBenutzerkontoSample();
            benutzerkontoPersistenceFixture.Persist(TestData.BenutzerkontoSample);
        }
        // ...                        
        protected override void Dispose(bool disposing)
        {
            if (!disposing)
            {
                return;
            }

            buchPersistenceFixture.DeletePersisted();
            // ...
        }
    }
}

Beachte, dass die Mocks sicherstellen, dass keine Infrastrukturabhängigkeit vorhanden ist.

Wie oben ersichtlich, wird der Service durch einen eigenen ContainerConfigurator (TestServiceHostFactoryConfigurator) bzgl. Dependency Injection konfiguriert. Dies ist bspw. notwendig, damit diese Tests nicht eine Schleupen-Infrastruktur, wie den ServiceBus, zur Ausführung benötigen. Denn dann können diese Tests bereits im CI-Test (siehe Teststrategie) ausgeführt werden.

Abgrenzung

Der ComponentTest ist kein Unittest (es wird also mehr als eine isolierte Klassen getestet) aber auch kein voll integrativer Test, um die Funktionalität in der vollständigen CS 3.0-Infrastruktur zu testen.

Stärken und Schwächen

Stärken
  • ComponentTest ohne Abhängigkeit zur Infrastruktur
  • Einfaches Debugging
Schwächen
  • Komplexität des Problems an sich
Cookie Consent mit Real Cookie Banner