Integration Tests Tutorial: Testautomatisierung auf verschiedenen Stufen – Teil 3

Integration Tests

Tutorial-Ziel:

Integration Tests stellen nach den Unit Tests die nächste zentrale Teststufe innerhalb der Testautomatisierungspyramide dar. In diesem Tutorial werden die Grundlagen der Integration Tests erläutert und verschiedene Arten von Integration Tests anhand von praktischen Beispielen demonstriert und geübt.

Unter anderem werden folgende Themen behandelt:

  • Was sind Integration Tests und ihre verschiedene Unterarten?
  • Welche Bereiche kann man mit Integration Tests abdecken?
  • Die grundlegenden Bestandteile eines Integration Tests
  • Erstellung verschiedener Integration Tests für das Beispiel-Szenario
  • Wartung und Lifecycle der Integration Tests

1. Was sind Integration Tests und ihre verschiedene Unterarten?

Integration Tests ergänzen die bereits begonnene Qualitätssicherung auf der Stufe der Unit Tests auf einer höheren Ebene. Das Ziel der Integration Tests besteht darin, wie der Namen schon andeutet, das Zusammenspiel von mehreren Software -„Einheiten“ zu verifizieren. Der schwammige Begriff „Einheit“ wurde hier bewusst gewählt, um die Vielseitigkeit des Verständnisses und der Anwendung der Integration Tests zu verdeutlichen. Und tatsächlich kann die Integration auf unterschiedlichen Granularitätsebenen erfolgen, so dass kaum ein anderer Begriff aus dem Bereich der Qualitätssicherung so stark durch die Interpretationen überladen ist wie die Bezeichnung „Integration Tests“.

Ich möchte an dieser Stelle erst gar nicht auf alle möglichen Synonyme und Bezeichnungen eingehen, sondern stattdessen die grundsätzlichen Unterarten von Integration Tests skizzieren – so vielfältig sie auch in der Praxis vorkommen.

Fasst man verschiedene Interpretation und Deutungen von dem Begriff Integration Tests zusammen, so lassen sich folgende Unterarten identifizieren, die in wesentlichen den Fokusbereich der jeweiligen Integration Tests beschreiben:

  • (Inner-Module) Integration Tests
  • (Cross-Module) Integration Tests
  • (Cross-Tier) Integration Tests
  • (Sub-System) Integration Tests
  • (System) Integration Tests

2. Welche Bereiche kann man mit Integration Tests abdecken?

Inner-Module Integration Tests:

Die Inner-Module Tests stellen die feingranularste Art der Integration Tests dar. Ihr Ziel besteht darin das Zusammenspiel mehrerer Klassen und Funktionen miteinander zu testen. Sie stellen damit eine logische Aggregation der Unit Tests dar und in der Tat ähnelt diese Unterart am stärkten den Unit Tests. Der Unterschied besteht darin, dass die zu testende Einheit nicht mehr einzelne atomare Funktionen oder Klassen umfasst, sondern sich auf eine bestimmte fachliche oder technische Funktionalität fokussiert, die sich erst aus dem Zusammenspiel mehrerer solcher atomaren Einheiten eines Moduls ergibt. Diese Art der Integration Tests wird häufig als White-Box Tests fast zeitgleich mit Unit Tests umgesetzt.

Folgende Skizze veranschaulicht den Fokus dieser Tests:

Inner-Modul Integration Tests

Cross-Module Integration Tests:

Mit der Cross-Module Integration versucht man das Zusammenspiel und damit auch die Schnittstellen zwischen mehreren unterschiedlichen Modulen bzw. Services zu testen. Damit kann man sehr zielgerichtet die Schnittstellenverletzungen identifizieren bzw. häufig auch  Missverständnisse zwischen Entwicklern aus verschiedenen Teams aufdecken, die vorhin einzeln an den jeweiligen Modulen gearbeitet haben. Diese Unterart der Integration Tests ist besonders in Bereich der SOA Architekturen sehr wichtig und verbreitet. Man könnte hier von einer horizontalen Integration sprechen.

Cross-Modul Integration Tests

Cross-Tier Integration Tests:

Die Cross-Tier Tests zielen darauf ab das Zusammenspiel der benachbarten „Schichten“ entlang der Multi-Tiered Software Architektur zu testen. Am häufigsten wird damit die Integration der Daten- (DB) und Business Logic (Application Server) Schicht getestet. Das ist eine besondere Variante der Cross-Module Tests, da hierbei genauso so 2 oder mehr zusammenarbeitende Module getestet werden, allerdings mit dem Fokus auf die einzelnen Schichten einer geschlossen mehrschichtigen Applikation. Man könnte hier von einer vertikalen Integration sprechen.

Cross-Tier Integration Tests

Sub-System Integration Tests:

In einigen Unternehmen versucht man mit Hilfe der Integration Tests das Zusammenspiel ganzer Teil-Systemlandschaften zu testen. Das geht deutlich über den Umfang der einzelnen Cross-Module Integration Tests hinaus, da hier viele Services und Applikationen (häufig sogar transitiv) miteinander vernetzt sind und komplexe Pfade der zu testenden Geschäftsabläufe abbilden.

Sub-System Integration Tests

System Integration Tests:

Immer wieder trifft man auch auf die Interpretation der Integration Tests als Tests, die auf höchster Systemebene das Zusammenspiel und den korrekten Ablauf der gesamten Geschäftsprozesse entlang der Systemlandschaft validieren wollen.

Persönlich finde ich diese Interpretation wenig gelungen, denn spätestens diese Art der Tests sollte eher durch die dedizierte Stufe der End-to-End Tests abgebildet werden, die in einem späteren Teil der Serie näher vorgestellt wird.

System Integration Tests

3. Die grundlegenden Bestandteile eines Integration Tests

Aufbau

Integration Tests sind strukturell ähnlich aufgebaut wie die Unit Tests und weisen folgende logische Bestandteile auf:

  1. Input: Bereitstellung der erforderlichen Eingabe-Daten
  2. Interaction: Aufruf der zu testenden Funktionseinheit
  3. Assertion: Verifikation der Ergebnisse nach dem Aufruf der Funktion mit den erwarteten Ergebnissen

Der Unterschied besteht in wesentlichen darin, dass bei Integration Tests die Logik, die die Testdaten bereitstellt oder die zu testende Funktion aufruft in der Regel viel komplexer und aufwändiger ist, als bei den einfachen atomaren Unit Tests. So müssen häufig ganze Datenstrukturen entweder aufwendig initiiert oder aus einer (gemockten) Datenquelle geladen werden. Auch die technischen Vorbereitungen, die erforderlich sind, um eine zu testende Funktionalität aufrufen zu können, bedürfen u.U. sogar weiterer Hilfskonstrukte und Funktionen, die in so einem Integration Test zum Einsatz kommen.

CI / CD Anbindung

Technisch werden die Integration Tests (insbesondere White-Box) häufig mit Hilfe der gleichen Testframeworks und programmiersprachlichen Konstrukte aufgebaut wie die Unit Tests. Effektiv kapseln die physischen Unit Test Methoden die Definition der Integration Tests, die dann durch Unit Test Frameworks bzw. CI System gestartet und ausgeführt werden. Um die Integration Tests von den „echten“ Unit Tests dennoch schnell unterscheiden zu können, bieten die meisten Unit Test Frameworks zusätzliche Kategorien an, mit denen die Tests markiert werden können. Auf diese Weise kann man die Strategien bei der Durchführung der (CI/CD) Tests besser steuern.

So kann man bei der konsequenten Markierung aller echten Unit Tests mit der Testkategorie „Unit Tests“ und der Integration Tests als „Integration Tests“ die Durchführung auf CI /CD System aufsplitten. Bei Check-ins kann das CI/Build System z.B. nur die Unit Tests der Kategorie „Unit Tests“ durch das Unit Test Framework laufen lassen. Bei Nightly-Builds beispielhaft zuerst nur die Unit Tests durchführen und dann, nur wenn alle Tests der Kategorie „Unit Tests“ bestanden haben, die Durchführung der physischen Unit Tests der Kategorie „“Integration Tests“ starten.

Tools

Neben der Möglichkeit Integrationstests programmatisch in einer Entwicklungsumgebung zu implementieren, gibt es auch sehr viele kommerzielle und Open Source Werkzeuge, die sich auf die Erstellung und Durchführung der Integration Tests ohne der Verwendung der klassischen Unit Test Framework Konstrukte spezialisiert haben. Einige Vertreter solcher Werkzeuge sind z.B. „SoapUI“ und „Postman“ für die Integration Tests der Services auf API Ebene. Solche Werkzeuge bieten eigene Oberflächen an, um (sogar ohne Programmierkenntissse) die Testszenarien zu verwalten, zu definieren und durchzuführen.

Partnerangebot: Praxisnahe Schulungen

Sie arbeiten im Testumfeld und möchten sich in C# / Integration Tests / Test Driven Development weiterbilden?

Wir führen in regelmäßigen Abständen praxisnahe Schulungen in diesem Bereich durch!
Demnächst statt findende Schulungen:

4. Erstellung verschiedener Integration Tests für das Beispiel-Szenario

4.1 Inner-Module Integration Tests schreiben

Bevor wir mit der Entwicklung des ersten Integrationstests fortfahren, bauen wir zuerst unsere Beispiel-Applikation ein wenig aus (damit wir etwas mehr zu integrieren haben:-).

Beispiel-Applikation erweitern

Öffnen Sie bitte die in Teil 2 erstellte C# Projektmappe in Visual Studio.

Mit Rechtsklick in Solution Explorer auf das Projekt „Multiplikator“ wählen Sie anschliessend den Menüpunkt „Hinzufügen“ -> „Klasse“ aus.

 

 

 

 

 

 

 

 

 

 

 

 

In dem daraufhin erscheinenden Dialog geben Sie „Calculator.cs“ als den Namen für unsere neue Steuerungsklasse ein.

Öffnen Sie mit Doppelklick die neue Calculator Klasse und ersetzen Sie ihr Inhalt mit folgender Implementierung:


namespace Multiplikator
{
    public class Calculator
    {
        public enum Operation
        {
            Multiplication,
            Division,
            Addition,
            Substraction
        }

        public static double Calculate(Operation operation, int operand1, int operand2)
        {
            double result = 0.0;
            switch (operation)
            {
                case Operation.Multiplication:
                    Multiplikator instance = new Multiplikator();
                    result = instance.Multiply(operand1, operand2);
                    break;

            }

            return result;
        }
    }
}

Die Klasse Calculator dient als eine zentrale Ansteuerungsklasse zur Durchführung verschiedener mathematischer Operationen.
Die jetzige Implementierung kann nur die geforderte Mindestanforderung erfüllen, nämlich Multiplikation zweier gerader Zahlen. Sie kann jedoch jederzeit um weitere Operationen erweitert werden.

Genau diese Ansteuerung wollen wir nun integrativ testen, um sicherzustellen, dass sie die gewünschte Funktionalität erfüllt.

Integration Test Projekt erstellen

Im nächsten Schritt fügen wir ein neues Integration Test Projekt hinzu. Klicken Sie dazu in Datei-Menü auf „Hinzufügen“ -> „Neues Projekt“.

Daraufhin öffnet sich ein Dialog, indem wir erneut als Typ ein Unit Test Projekt für unser Vorhaben selektieren.

Wählen Sie bitte „Visual C#“ -> „.Net Core“ -> „MSTest Test Projekt (.Net Core)“ aus und nennen Sie dieses Projekt „IntegrationTests“.

Anschliessend nennen wir die vordefinierte Unit Test Klasse von „UnitTest1.cs“ nach „CalculatorTests.cs“ um und bestätigen die Aufforderung mit „Ja“.

Um die Vorbereitung abzuschliessen müssen wir noch die Implementierung unserer Beispiel-Applikation im neuen Integration Test Projekt referenzieren.

Das geschieht mit Rechtsklick auf „Abhängigkeiten“ des Integration Test Projektes und dann auf „Referenz hinzufügen …“.

Im Auswahl-Dialog aktivieren wir die Check-Box für das Projekt“ Multiplikator und bestätigen mit OK.

Mit Doppelklick auf die Datei „CalculatorTests.cs“ öffnet sich die Klasse in VS Editor.

Man erkennt an der Vorgehensweise und der Struktur der Testklasse bereits die anfangs erwähnte Tatsache, dass Integration Tests aus technischer Sicht vielfach als Unit Tests implementiert werden.

 

Inner-Module Integration Test einfügen

Nun fügen wir in die Klasse „CalculatorTests“ folgenden Integrationstest ein, der als Unit Test Methode deklariert wird, aber über die Kategorie explizit auf den fachlichen Inhalt als „Integration Test“ hinweist.

        [TestMethod]
        [TestCategory("Integration Test")]
        public void TestMutliplication()
        {
            // 1. Input
            int operand1 = 2;
            int opernad2 = 3;

            // Expected
            double expResult = 6;

            double result = Calculator.Calculate(Calculator.Operation.Multiplication, operand1, opernad2);

            // 3. Validation
            Assert.AreEqual(expResult, result);
        }

Die Implementierung ruft die statische „Calculate“ Methode unserer neuen Ansteuerungsklasse „Calculator“ auf und testet ihre korrekte Funktionalität hinsichtlich der Multiplikation.

Der neue Integration Test ist fast einsatzbereit. Füge Sie nur noch die fehlende using Direktive am Anfang der Klasse ein.


using Multiplikator;

Jetzt ist die Implementierung fertig. Mit F6 können Sie das Projekt kompilieren und den neuen Testfall über den „Test Explorer“ ausführen.

Unser erster Integration Test innerhalb eines Moduls ist fertig. Er testet das Zusammenspiel der Ansteuerungsklasse „Calculator“ mit der Klasse „Multiplikator“. Technisch wurde dieser Test als Unit Test implementiert.

 

4.2 Cross-Tier Integration Tests schreiben

Um einen Integrationstest zu schreiben, der das Zusammenspiel mehrerer Module testet, müssen wir auch an dieser Stelle unsere Beispiel Applikation „ein wenig“ aufbohren.

Web Server für Beispiel-Applikation anlegen

Dazu erstellen wir ein neues Web Server Projekt, das unsere Multiplikationsbibliothek verwendet und später über Web verschiedenen Konsumenten bereitstellen kann.

Klicken Sie in Datei-Menü auf „Hinzufügen“ -> „Neues Projekt“.

Daraufhin öffnet sich erneut ein Dialog, indem wir diesmal den Typ „.NET Core“ -> „ASP.NET Core Web Application“ für die Erstellung eines Web Servers auswählen. Bitte nennen Sie das Projekt „CalcServer“ und bestätigen Sie den Dialog mit OK.

Im nächsten Dialog müssen noch ein Paar technische Details für die Generierung eines vordefinierten Web Servers spezifiziert werden.

Bitte selektieren Sie „Angular“ aus und deaktivieren Sie die Check-Box „Configure for HTTPS“ und bestätigen Sie mit OK.

Nach kurzer Wartezeit wird ein neues WebServer Projekt generiert und zur Solution hinzugefügt. Durch Rechtsklick auf Projekt „CalcServer“ setzen Sie den Web Server über Menüpunkt „Set as Startup Projekt“ als Startprojekt.

Web Service für die Kalkulationen erstellen

Um unseren Web Server mit sinnvollen Funktionalitäten zu füllen, erstellen wir einen neuen Web Service, der die Calculator-Funktionalitäten nach aussen allen Web-Konsumenten bereitstellt.

Da der WebService auf unser Modul (Library) mit der implementierten Multiplikation zugreifen soll, fügen wir analog zur obigen Vorgehensweise das Projekt „Multiplikator“ als Abhängigkeit hinzu. Rechtsklick auf „Abhängigkeiten“ des CalcWebServer Projektes, dann auf „Referenz hinzufügen …“ und Multiplikator Projekt einchecken.

Nun klicken Sie in „CalcServer“ Projekt mit Rechtsklick auf den Ordner „Controller“ und fügen Sie über Kontextmenü „Hinzufügen“ -> „Neue Klasse“ eine neue Klasse mit der Bezeichnung „CalcController“ hinzu.

An dieser Stelle möchte ich nicht auf die Details der Web Server Programmierung eingehen (das wäre ein eigenständiges  Tutorial), sondern stattdessen die fertige Implementierung anbieten, mit der Sie den Inhalt der neuen Klasse überschreiben:

using Microsoft.AspNetCore.Mvc;
using Multiplikator;
using System;

namespace CalcServer.Controllers
{
    [Route("api/calc")]
    public class CalcController : Controller
    {
        [HttpPost]
        public IActionResult Calculate([FromBody] CalcData calcData)
        {
            try
            {
                double result = Calculator.Calculate(calcData.Operation, calcData.Operand1, calcData.Operand2);

                return Ok(result);
            }
            catch (Exception)
            {
                return BadRequest("Invalid operation");
            }
        }
    }

    public class CalcData
    {
        public int Operand1 { get; set; }
        public int Operand2 { get; set; }
        public Calculator.Operation Operation { get; set; }
    }
}

Damit ist die Implementierung auf der Web Server Seite erst mal abgeschlossen und wir können gleich probieren die Implementierung über Integration Tests zu verifizieren – sogar bevor wir den Web Server hochfahren!

Cross-Tier Integration Test erstellen

Mit diesem Test wollen wir verifizieren, dass die Implementierung der „Calculate“ Methode des Web Services korrekt mit unserer Multiplikation-Bibliothek zusammenarbeitet und das richtige Ergebnis zurückliefert. Die Bibliothek  „Multiplikation“ wird in diesem Beispiel stellvertretend für eine in der Praxis übliche komplexe Bibliothek auf unterer Schicht wie z.B. Datenbankzugriffsbibliothek verwendet.

Wie würde der entsprechende Integration Test auf dieser Ebene aussehen?

Nun, als erstes fügen wir in unserem Integration Test Projekt eine Verknüpfung zum neuen CalcServer Projekt hinzu (Rechtsklick auf „Abhängigkeiten“ -> „Referenz hinzufügen“ -> „CalcServer“ einchecken -> OK).

Als nächstes erstellen wir eine Kopie der Testklasse „CalculatorTests“

  • Rechtklick auf „CalculatorTests.cs“ in Solution Explorer -> Kopieren.
  • Rechtklick auf Integration Test Projekt in Solution Explorer -> Einfügen.
  • Die neue Datei in Solution Explorer in „CalcServiceTests.cs“ umbenennen und den Inhalt der Testklasse mit folgender Implementierung ersetzen:
using CalcServer.Controllers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Multiplikator;

namespace IntegrationTests
{
    [TestClass]
    public class CalcServiceTests
    {

        [TestMethod]
        [TestCategory("Integration Test")]
        public void TestMutliplication()
        {
            // 1. Input 
            CalcData inputData = new CalcData();
            inputData.Operand1 = 2;
            inputData.Operand2 = 3;
            inputData.Operation = Calculator.Operation.Multiplication;

            // Expected 
            double expResult = 6;

            // 2. Execution
            CalcController calcService = new CalcController();
            var response = calcService.Calculate(inputData) as ObjectResult;
            double result = double.Parse(response.Value.ToString());

            // 3. Validation 
            Assert.AreEqual(expResult, result); 
        }
    }
}

Fertig. Mit F6 kompilieren. In Test Explorer den neuen Test ausführen.

Kein Hochfahren des Web Servers notwendig! Auf diese Weise haben wir einen White-Box Integration Test unmittelbar nach der Implementierung geschrieben, der in unserer lokalen Entwicklerumgebung sofort prüft, ob der neu WebService des WebServers mit der Multiplikation Bibliothek richtig zusammenarbeitet.

Well done!

Mit unserem Cross-Tier Integration Test konnten wir zwar sehr effizient die Integration einer tieferliegenden Schicht (abgebildet durch eine Business Logik Bibliothek) testen, dennoch bleiben viele Aspekte der Integration gar nicht überprüft. Ist unser Web Service überhaupt lauffähig? Können externe Konsumenten darauf zugreifen?

Um diese Fragen zu beantworten werden wir im folgenden Abschnitt einen  Cross-Module Integration Test aufsetzen, mit dem das Zusammenspiel mit einem echten Konsumenten geprüft wird.

 

4.3 Cross-Module Integration Tests schreiben

Im Rahmen eines Cross-Module Integration Tests wollen wir das reale Zusammenspiel der Module verifizieren,  indem der Testfall einen echten Konsumenten des Modules (unser CalcService) nachbildet und so prüft, ob der CalcService

  • lauffähig und erreichbar ist
  • der Aufruf mit den spezifizierten Eingabeparametern wie erwartet funktioniert
  • das zurückgelieferte Ergebnis dem für Konsumenten erwarteten Ergebnis entspricht

Diese Art der Integration Tests kann man sowohl

  • programmatisch, gekapselt in einem Unit Test Container, umsetzen
  • als auch mittels externer Testautomatisierungstools, wie am Anfang des Tutorials erwähnt, aufbauen.

In folgenden Abschnitten werden beide Alternativen anhand unseres Beispiels demonstriert.

 

4.3.1 Cross-Module  Integration Test in IDE programmieren

In Unterschied zu unserem leichtgewichtigen Cross-Tier Integration Test im vorherigen Abschnitt, wollen wir mit dem neuen Cross-Module Test unseren Web Server mit dem bereitgestellten CalcService tatsächlich physisch hochfahren und aus der Perspektive eines Konsumenten (eines anderen Moduls) testen.

Glücklicherweise bietet das Unit Testing Framework zusätzliche Bibliotheken zur Vereinfachung der Integration Tests an, die uns ermöglichen den Web Server direkt aus dem Unit Test heraus ad-hoc hochzufahren, ohne umständlich den Web Server extern starten zu müssen und mit der Testdurchführung zu synchronisieren.

Das geht so:

Synthetischen Web Server hinzufügen und konfigurieren

Fügen Sie zu unserem CalcServer Projekt ein neues Paket hinzu:

    1. Rechtklick auf CalServer->Abhängigkeiten
    2. Menüpunkt „Nuget Pakete verwalten“ auswählen
    3. Es erscheint ein Fenster des Nuget-Paket Managers, in dem wir in Tab „Browse“ nach dem Paket „Microsoft.AspNetCore.TestHost“ suchen.
    4. Markieren Sie anschliessend das gefundene Paket und klicken Sie auf der rechten Seite auf die Taste „Install“
    5. Bestätigen Sie ggf. die beiden Nachfragen mit „OK“.

Wiederholen Sie die Schritte 1-5 auch für das Paket „Microsoft.AspNetCore.App“, das ebenfalls benötigt wird.

Das Paket „TestHost“ bietet uns eine mächtige Funktionalität an, aus einem physischen Unit Test (der Kategorie  „Integration Test“) einen spezifizierten Web Server hochzufahren und nach der Durchführung der Tests automatisch herunterzufahren.

Da wir die Funktionalität zum Hochfahren des WebServers höchstwahrscheinlich an mehreren Stellen in unserem Integration Test Projekt benötigen werden, lagern wir die Hilfsmethoden dazu von Anfang an in eine Hilfsklasse aus:

  1. Rechtklick auf „Integration Test“ Projekt
  2. Im Kontextmenü „Hinzufügen“-> „Klasse…“ auswählen
  3. Als Namen „WebSerberHostHelper.cs“ eingeben und mit OK bestätigen.

Öffnen Sie die neue Klasse „WebSerberHostHelper“ und ersetzen Sie Ihr Inhalt mit folgendem Code:


using CalcServer;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using System.Net.Http;

namespace IntegrationTests
{
    public class WebServerHostHelper
    {
        public WebServerHostHelper()
        { }

        public HttpClient GetClient()
        {
            TestServer testServer = StartupServer();
            HttpClient client = testServer.CreateClient();
            return client;
        }

        private TestServer StartupServer()
        {
            IWebHostBuilder webHostBuilder = WebHost.CreateDefaultBuilder(new string[] { })
                                .UseStartup<Startup>().UseEnvironment("Development")
                                .UseKestrel();

            TestServer testServer = new TestServer(webHostBuilder);
            return testServer;
        }
    }
}

 

Web Service Test schreiben

Nun sind wir mit den Vorbereitungen fertig und können die eigentliche Testimplementierung in die Klasse „CalcServiceTests“ als zweiten Testfall einfügen:

 

        [TestMethod]
        [TestCategory("Integration Test")]
        public void TestMulitplicationService()
        {
            // 1. Input 
            CalcData inputData = new CalcData();
            inputData.Operand1 = 2;
            inputData.Operand2 = 3;
            inputData.Operation = Calculator.Operation.Multiplication;

            System.IO.StringWriter requestPayloadWriter = new System.IO.StringWriter();
            Newtonsoft.Json.JsonSerializer js = new Newtonsoft.Json.JsonSerializer();
            js.Serialize(requestPayloadWriter, inputData);

            // Expected 
            double expResult = 6;

            // 2. Execution
            WebServerHostHelper helper = new WebServerHostHelper();
            System.Net.Http.HttpClient client = helper.GetClient();

            System.Net.Http.HttpContent content = new System.Net.Http.StringContent(requestPayloadWriter.ToString(), System.Text.Encoding.UTF8, "application/json");
            var response = client.PostAsync("/api/calc", content).Result;


            // 3. Validation 
            Assert.AreNotEqual(null, response);
            Assert.AreEqual(true, response.StatusCode == System.Net.HttpStatusCode.OK);
            string responseContent = response.Content.ReadAsStringAsync().Result;
            double actualvalue = double.Parse(responseContent, System.Globalization.CultureInfo.InvariantCulture);
            Assert.AreEqual(expResult, actualvalue);
        }

Vergleich mit feingranularer Integration

Auf ersten Blick erkennen wir, dass die Implementierung strukturell sich kaum verändert hat. Nach wie vor verwenden wir eine Unit Test Hülle um einen Integration Test zu kapseln. Auch die Bestandteile des Tests sind nach wie vor die gleichen wie bei anderen Unit und Integration Tests aus diesem Tutorial. Nur die Komplexität dieser Bestandteile ist nun ein wenig gestiegen.

Die Input Behandlung erfordert nun eine zusätzliche Serialisierung der Eingabedaten in das JSON Format, weil das die technische Grundlage für die Kommunikation mit unserem Web Server darstellt.

Der eigentliche Aufruf des Web Services erfordert in Vorfeld das Hochfahren eines synthetischen Servers über unsere ausgelagerte Hilfsmethode, die uns anschliessend eine Instanz des HTTP Clients zurückgibt. Dieser HTTP Client stellt einen leichtgewichtigen Browser dar, der das Verhalten des Konsumenten simuliert und die Anfrage an den Web Server sendet.

Auch bei der Validierung der Antwort müssen wir (und wollen wir) noch einige zusätzliche Aktionen durchführen. Als erstes gilt es zu verifizieren, ob der Web Server uns ein Ergebnis mit dem erwarteten Status Code „OK“ für die fehlerfreie Verarbeitung zurückgibt. Dadurch wissen wir, dass

  • der Web Server läuft
  • die Anfrage entgegennehmen und an einen (nicht unbedingt richtigen :-)) Service weiterleiten kann
  • ein technisches Ergebnis zurückliefert

In weiteren Schritten versuchen wir aus der technischen Antwort des Servers das erwartete fachliche Ergebnis durch erneute Transformation herauszulesen und zu prüfen, ob dieses fachliche Ergebnis mit dem erwarteten Wert übereinstimmt.

Dieser Integration Test ist um einiges komplexer geworden, erlaubt uns aber deutlich mehr Aspekte unserer Modulintegration zu testen und dabei trotzdem noch vergleichsweise leichtgewichtig und schnell bleibt, da wir keinen echten Web Server hochfahren, sondern einen synthetischen Web Server dynamisch instanziieren.

 

4.3.2 Cross-Module  Integration Test mit API Test Tool bauen

Versuchen wir als nächste Übung exakt den gleichen Test zum Vergleich mit einem externen Testtool zusammenzubauen, um zu verdeutlichen, dass viele Tests auf höheren Ebenen auch als Black-Box Tests ohne zwingende Nutzung einer Programmiersprache und Code-Zugriff entwickelt werden können. Für unser Beispiel verwenden wir ein weit verbreitetes kostenloses Werkzeug „Postman“ zum Testen von Web Services und REST APIs, das sehr einfache und für den Einstieg gut geeignete Oberfläche anbietet.

Ein ausführliches Tutorial zum Testen von REST APIs mit Postman finden Sie hier.

Erste Schritte mit Postman

1. Laden Sie, installieren Sie und starten Sie anschliessend Postman lokal auf Ihrem Computer

2. Beim ersten Start erscheint ein Dialog in dem Sie die Option „Create a basic request“ auswählen

3. Im nächsten Fenster geben wir den Namen „TestMulitplicationService“ für unseren neuen Test ein. Dann erstellen wir einen neuen Ordner „Test“, in dem der Testfall gespeichert wird und selektieren diesen Ordner. Anschliessend klicken wir auf „Save to Test“, woraufhin Postman eine einfache Vorlage für unseren Request erstellt und die Hauptoberfläche zum Editieren öffnet.

4. Im nächsten Schritt klappen wir auf der Oberfläche den neuen Testordner auf und selektieren den Testfall „TestMulitplicationService“. Auf der rechten Seite öffnet sich der Request-Editor, in dem wir die Eigenschaften für den Web Service Aufruf eintragen können. Als erstes wählt man den Typ der API Methode, die wir aufrufen wollen. Da unser CalcService eine POST Anfrage mit zugehörigem JSON Payload erwartet, ändern wir den Typ von „GET“ auf „POST“. Als nächstes tragen wir die URL unseres Web Services ein …

5. … doch halt – da war noch was! Wir wissen noch gar nicht wo und über welche URL unser Web Service erreichbar ist. Und das, nachdem wir bereits ausgiebig verschiedene Funktionalitäten des implementierten Multiplikation-Features über vorhergehenden Unit und Integration Tests getestet haben. Tatsächlich benötigen wir erst an dieser Stelle

  • einen echten Server(lokaler Rechner, VM, Hardware Server oder Cloud Server)
  • auf dem unser WebServer (IIS, Nginx oder ASP.NET Core Standalone Web Server) läuft
  • und den vorher implementierten, kompilierten und bereitgestellten Web Service samt der Bibliothek für die Multiplikation hostest
Profi-Tipp:

Uff, ich hoffe die obige Auflistung verdeutlicht wie schnell sich die Anforderungen an die Infrastruktur und Umgebung (mit den daraus resultierenden Aufwänden, der Komplexität und Fehlerwahrscheinlichkeit) steigen, wenn die Tests auf höheren Stufen erfolgen. Dann lernt man schnell zu schätzen, dass man durchaus (sehr) viele sinnvolle Tests auf tieferen Stufen implementieren kann und ja, sogar unbedingt sollte!

Und wenn wir schon über das Thema sprechen: bezogen auf unser konkretes Testszenario mit 3 vorgeschlagenen Testfällen könnte ein aufmerksamer Leser sich fragen, ob es denn nicht möglich wäre die eigentliche Korrektheit der Multiplikation anhand der 3 Testfälle bereits sehr früh auf Ebene der Unit und Inner-Modul Integration Tests zu testen, und die korrekte Bereitstellung dieser Funktionalität über den Web Service nur mit einem einzigen Testfall abzudecken, statt erneut wieder die gleichen Konstellationen zu prüfen.

Die Antwort ist ganz klar: ja, kann man und sollte es tun(!). Das ist etwas, was in gut organisierten cross-funktionalen agilen Teams auch passiert bzw. passieren sollte, indem die Tester und Entwickler im engen Austausch miteinander stehen und die Testfälle auf verschiedenen Teststufen in Bezug auf Relevanz, Risiko und Redundanz abstimmen und dadurch viele unnötige (teure und fehleranfällige) Tests auf höheren Teststufen sich ersparen können (dazu später mehr).

Lokalen Web Server konfigurieren und starten

6. Erfreulicherweise können wir für die Demo-Zwecke den Web Server erstmal lokal aus der Entwicklungsumgebung heraus starten, weil wir im Vorfeld eine gültige minimalistische Web Server Implementierung erstellt haben. Dazu laden wir in Visual Studio (sofern nicht offen) unsere Multiplikator-Projektmappe. Dann legen wir mit Rechtsklick auf „CalcServer“ Projekt in Solution Explorer dieses Projekt als Startprojekt fest. Anschliessend selektieren wir aus dem Drop-Down Menü der Run Taste den Stand-Alone Modus des „CalcServer“ für den Betrieb von unserem Web Server (statt IIS Express Hosting) aus.

7. Mi Klick auf die grüne „Run“ Taste starten Sie schliesslich die realle SuT ( Web Server Applikation) in einem Batch-Fenster (optisch sichtbar). Es dauert ein Paar Sekunden bis der Web Server hochgefahren ist. Das erkennt man an der Ausgabe „Application started. Press Ctrl+C to shut down.“ im Batch Fenster. Eine Zeile höher finden wir auch die für uns wichtige Information über die Adresse des Web Servers. In meinem Fall handelt es sich um diese URL: „http://localhost:12290“. Aber Achtung, auf Ihrem System kann z.B. der Port anders lautet.

Postman Testfall zusammenstellen

8. Endlich läuft unser echter Web Server und wir haben die technische Information über die URL, über die er erreichbar ist. Damit wechseln wir wieder zurück zum Postman und tragen in der URL-Zeile die tatsächlcihe URL unseres Web Servers ein und ergänze sie um den Service Endpunkt, etwa so:

http://localhost:12290/api/calc

9. Was nun fehlt ist die Spezifikation der Eingabeparameter in JSON Format in der „Body“ der Anfrage. Dazu klicken Sie auf den Tab „Body“.

10. Als Format wählen wir „raw“ und Medienformat JSON (application/json)

11. Abschliessend fügen wir folgenden JSON Block als Inhalt des Requests ein:

{
"operand1":2,
"operand2":3,
"operation":0
}

12. Mit dem Klick auf „Send“ wird die Anfrage an den Web Server gesendet. Im unterem Fenster erhalten wir den erwarteten Ergebniswert 6.

13. Um das Beispiel abzuschliessen fehlen uns noch 2 Assertions. Um diese hinzuzufügen wechseln wir in Tab „Tests“ und selektieren dort aus dem Snippet Fenster auf der rechten Seite folgende vorgefertigte Checks:

  • „Status Code: Code is 200“
  • „Response Body: Contains string“ und ersetzen im eingefügten Check den Textplatzhalter durch den erwarteten Ergebniswert „6.0“

 

14. Wiederholen Sie den Test durch Klick auf „Send“. Nun werden auch die neuerstellten Prüfungen automatisch angewandt.

Vergleich der Automatisierungslösungen

Der Test ist nun erfolgreich durchgelaufen und entspricht dem gleichen Test, den wir in IDE programmiert haben. Diesmal haben wir jedoch ein interaktives Tool verwendet um den Testfall zusammenzustellen? Welche Möglichkeit fanden sie leichter? Welche sagt Ihnen mehr zu? Wichtig ist an dieser Stelle festzustellen, dass die Integration Tests auf API Ebene (wie in unserem Beispiel) selbst bei der Verwendung der interaktiven Testwerkzeuge  ein solides technisches Verständnis für SuT erfordern (hier Web Services, RestAPI, JSON), obwohl wir effektiv keine einzige Zeile Code schreiben mussten.

Die Tests, die wir in einer Entwicklungsumgebung implementiert haben, ermöglichen uns weiterhin eine flexible Anpassung an spezielle Projektanforderungen, falls das benötigt wird. Beide Umsetzungen können in eine CI/CD Pipeline integriert werden, wobei die Anbindung externer Testwerkzeuge in CI sich stark unterscheiden kann. Entweder geschieht das über die Kommandozeile oder REST API des jeweiligen Werkzeugs.

4.4 (Sub-)System Integration Tests implementieren

Wie an Anfang dieses Tutorial-Teils erläutert, versucht man mit Hilfe der (Sub-)System Integration Tests einen noch grösseren Domänenbereich mit mehreren abhängigen und zusammenarbeitenden Modulen abzudecken. Die prinzipielle Vorgehensweise ähnelt dabei sehr stark den vorgestellten Techniken bei der Umsetzung der Cross-Module Tests. Auch hier kann man sowohl auf die open-source und kommerziellen Testtools zurückgreifen als auch die Tests programmatisch in einer Entwicklungsumgebung durch die Nutzung zusätzlicher Frameworks und Treiber implementieren. Aus diesem Grund muss hier nicht explizit auf eine besondere Vorgehensweise bei der Umsetzung solcher Tests eingegangen werden.

Wichtig ist nur ein Aspekt: durch den angestiegen fachlichen Umfang der Testszenarien dieser Unterart erhöht sich zwangsläufig auch die technische Komplexität der zugehörigen Tests. Sie bestehen i.d.R aus einer Folge von mehreren Schritten, die stark aufeinander aufbauen und die Eingabe- und Ausgabe-Daten untereinander austauschen bzw. teilweise auch transitiv verarbeiten müssen. Aus diesem Grund steigt bei diesen Tests die Fehlerwahrscheinlichkeit (bedingt durch Implementierungsfehler, höhere Daten- und Umgebungs-Abhängigkeiten, längere Laufzeiten u.v.m.)  signifikant an. Versuchen Sie die Menge der Tests auf dieser Stufe möglichst gering zu halten und fokussieren Sie sich dabei auf die Tests der Kern-Workflows statt Permutation der Szenarien aller beteiligten Module.

 

5. Wartung und Lifecycle der Integration Tests

Die Wartbarkeit der Integration Tests unterscheidet sich signifikant abhängig von der Granularität (s. vorgestellte Unterarten), auf welcher sie umgesetzt wurden. Die Inner-Module und Cross-Tier Tests, die als White-Box Integration Tests in der gleichen Umgebung wie die zu testenden Applikationsbereiche  implementiert wurden, unterliegen typischerweise dem gleichen „schnellen“ Lifecycle“ wie die Unit Tests. Die meisten Code-Änderungen führen unmittelbar dazu, dass auch der Code der Integration Tests angepasst werden muss. Die zeitnahe Testdurchführung hilft dann sowohl die Fehler rasch zu finden als auch die zerschossenen Tests, z.B. durch die Anpassung der Eingabedaten, zum Zeitpunkt zu fixen als der Entwickler die Änderung noch in Kopf hat.

Mit steigender Granularität der Tests steigt die Wahrscheinlichkeit, dass die Wartung und Anpassung dieser Tests aufgrund der Code-Änderung erst zu einem recht späten Zeitpunkt beim ersten Deployment und Testlauf auf einer CD/Testumgebung erfolgt, so dass die Tester und Entwickler ein Paar zusätzliche Schleifen drehen müssen, bis die relevanten Änderungen nachvollzogen und nachgezogen werden können.

 

Zusammenfassung

In diesem doch recht langem Tutorial-Teil haben Sie den Fokus und verschiedensten Facetten der Integration Tests sowohl methodisch als auch praktisch anhand unseres kleinen Beispiels kennengelernt und ausprobiert. Die Vor- und Nachteile der Integration auf verschiedenen Granularitätsstufen wurden ebenfalls veranschaulicht. Versuchen Sie nun die gelernte Vorgehensweise auf weitere Applikationen / Module in Ihrem Bereich anzuwenden.

Im nächsten Teil der Serie beschäftigen wir uns mit den End-to-End Tests …

Serienteile:

0 Antworten

Hinterlassen Sie einen Kommentar

Wollen Sie an der Diskussion teilnehmen?
Feel free to contribute!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.