Unit Tests Tutorial – Testautomatisierung auf verschiedenen Stufen – Teil 2

Unit Tests

Tutorial-Ziel:

Im 1. Teil der Serie wurden die grundlegenden Testautomatisierungsansätze und Test Stufen sowie unser Beispiel Testszenario vorgestellt.

Im 2. Teil der Serie werden die Grundlagen der Testautomatisierung auf der Stufe der Unit Tests anhand dieses Testszenarios näher erläutert.

Unter anderem werden folgende Themen behandelt:

  • Was sind die Unit Tests und deren USPs?
  • Was soll mittels Unit Tests getestet werden?
  • Die grundlegenden Bestandteile eines Unit Tests?
  • Welche Unit Tests Frameworks gibt es?
  • Einrichtung einer Entwicklungsumgebung
  • Erstellung eines Unit Test Projektes
  • Erstellung der Unit Tests für das Beispiel-Szenario
  • Wartung und Lifecycle der Unit Tests

1. Was sind Unit Tests?

Unit Tests überprüfen auf technischer Ebene, ob der von den Software Entwicklern geschriebener Code-Fragment ordnungsgemäss arbeitet. Sie werden als fein-granulare atomare isolierte Tests konzipiert, die die kleinsten Einheiten des implementierten Codes (auf Ebene einzelner Funktionen und Klassen) testen. Bei der Implementierung der Unit Tests versucht macht möglichst alle Abhängigkeiten von anderen Klassen, Modulen und Daten zu mocken, damit solche Tests möglichst unabhängig, parallel und effizient in einer Unit Test Laufzeitumgebung (z.B. in IDE oder auf einem Build Server) laufen können.

Unit Tests werden überwiegend als White-Box Tests implementiert, die sowohl die positiven als auch negativen Interaktionspfade und Zustände in den jeweiligen Code-Einheiten abdecken.

Hinweis:

Sehr häufig werden die technischen Mittel für die Durchführung der Unit Tests dafür verwendet auch die Tests auf höheren Stufen, u.a. Oberflächentests, als Unit Tests zu „verpacken“, damit diese analog zu den „echten“ Unit Tests auf die gleiche Art und Weise bequem über eine IDE, Build Server oder Continuous Integration Umgebung gestartet werden können. Solche Tests werden dann tatsächlich (aus technischer Sicht) als Unit Tests abgebildet, weil sie die Unit Tests als physikalische „Container“ nutzen (s. u. Aufbau von Units Tests). Methodisch sollten sie dennoch nie mit den echten Unit Tests verwechselt werden, weil mindestens 2 charakteristische Merkmale von Unit Tests nicht erfüllt sind:

  • Testfokus auf kleine atomare technsiche Einheiten eines Code-Fragmentes statt auf ganze fachliche Funktionalitäten (die sich i.d.R. aus mehreren atomaren Einheiten zusammensetzen)
  • Direkter Aufruf der Code-Elemente (Klassen, Funktionen) statt Interaktion mit gebauter und lauffähiger Applikation (z.B. Web Server)

Ziele der Unit Tests:

Das übergeordnete Ziel ist es mit Hilfe der Unit Tests alle wichtigen Bestandteile der Implementierung mit hoher Testabdeckung zu testen. Je nach Geschäftsanforderungen und Kritikalität des Software-Produktes variieren die Anforderungen an die minimale sinnvolle Code Coverage über die Unit Tests.

Für normale nicht-kritische Software-Produkte ist ein Line Code Coverage von mindestens 60% anzustreben. Im Umfeld kritischer Systeme wie z.B. Medizintechnik werden üblicherweise deutliche höhere Unit Test Code Coverage Werte mit Branch Coverage von über 85% verlangt.

Tipp: Die reine nummerische (quantitative) Code Coverage darf nie als Merkmal zur Attestierung einer guten SW-Qualität herangezogen werden (selbst bei Werten von über 90%). Denn die nummerische Code Abdeckung liefert leider keinerlei Grundlage über die Abschätzung der Qualität der durchgeführten Unit Tests. So gilt es zusätzlich sicherzustellen, dass Unit Tests die relevanten Prüfungen (Assertions) enthalten (s.u.) und die erforderlichen Datenkonstellation / Äquivalenzklassen abdecken. Deshalb soll  die Test Code Coverage umgekehrt als Indikator für eine unzureichende Qualitätssicherung auf der Unit Test Ebene wahrgenommen werden, woraus dann Handlungsaktionen abgeleitet werden.

Vorteile der Unit Tests:

Der große Vorteil der Unit Tests besteht darin, dass man sie in einer sehr frühen Software-Entwicklungsphase unmittelbar während der Entwicklung schreiben und zur sofortigen Überprüfung der gerade programmierten Code-Stücke einsetzen kann.

Damit können Unit Tests auch unfertige und nicht-lauffähige Software Komponenten testen. Die einzige Voraussetzung bei den meisten Programmiersprachen ist, dass das zu testende SW Modul kompilierfähig ist.

Aufgrund der geringen ( oder im Optimalfall sogar gar keinen) Abhängigkeiten von anderen Modulen und Daten laufen die Unit Tests nicht nur blitzschnell sondern in der Regel sehr stabil mit äußerst geringer Flaky-Rate. Das liegt daran, dass wegen der hohen Isolation es kaum externe Ereignisse gibt, die das Verhalten bzw. die Stabilität beeinflussen können (wie z.B. Änderung der Datenbasis in der Datenbank, Nicht-Verfügbarkeit eines Web-Services u.v.m.)

2. Was soll mittels Unit Tests getestet werden?

Aufgrund der hohen Isolation und Feingranularität der Unit Tests sind sie dafür prädestiniert die technische Implementierung einzelner Funktionen, die z.B. wichtige fachliche Algorithmen kapseln, zu testen. Mit Hilfe der Unit Tests ist es möglich nicht nur alle fachlichen Pfade innerhalb einer Funktion zu testen, sondern vor allem auch die technischen Grenzfälle zu verifizieren.

So kann man z.B. prüfen, ob die Funktion korrekt auf ungültige Werte außerhalb des Wertbereichs und unerwartete interne Zustände reagiert. Genau so wichtig ist auch die Verifikation, ob die Behandlung interner Betriebssystem-Fehler, wie z.B. Netzwerk-Socket geschlossen, greift. Gerade das letzte Beispiel lässt sich mit Unit Tests sehr zielgerichtet und vergleichsweise mit geringem Aufwand testen, während die gleichen Tests auf höheren Stufen u.U. deutlich aufwändiger sind oder gar nicht möglich sind, weil die Fehlerereignisse gegebenenfalls durch höhere Schichten abgefangen oder maskiert werden.

Darüberhinaus ist es mit Unit Tests möglich sogar die internen nicht-öffnetlichen Methoden einer Klasse bzw. eines Obektes explizit zu testen, sofern diese Unit Tests als White-Box Tests entwicklet werden und den zu testenden Code referenzieren. Mit den automtatisierten Tests auf höheren Stufen ist nur ein indirekter Test solcher Methoden möglich.

Trotz des Scharms mittels Unit Tests viele interne Funktionalitäten testen zu können, ist es wichtig bei der Auswahl der zu testenden Code Bereiche zuerst stets die wichtigsten Teile (z.B. Kernfunktionalitäten aus der Business Sicht mit hoher Frequenz der Aufrufe) zu präferieren, damit die Unit Tests immer hohe Relevanz aufweisen. Das ist wichtig, weil mit steigendender Code Abdeckung der Aufwand für eine weitere Erhöhung der Code Coverage tendenziell exponentiell steigt und man unwirtschaftliche Aufwände reduzieren möchte.

 

3. Die grundlegenden Bestandteile eines Unit Tests?

Jeder Unit Test besteht aus folgenden Bestandteilen:

  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

Profi-Tipp:

Das klingt banal, doch gerade der 3. Teil wird in der Praxis entweder komplett vergessen/ausgelassen oder nur ungenügend abgedeckt. Dabei ist die Verifikation der Ergbnisse doch der zentrale Bestandteil der Unit Tests. Ohne solcher Verifikation sind die Unit Tests in den meisten Fällen wertlos. Schließlich wollen wir nicht nur prüfen, ob die zu testende Funktion ohne Exceptions durchläuft, sondern tatsächlich ihr fachliches Ziel erfüllt. Dazu ist es wichtig so viele Assertions wie möglich (aber so wenig wie nötig) in den Unit Tests zu implementieren. Bitte sparen Sie nicht an dieser Stelle – Sie werden dafür später ganz bestimmt dankbar sein!

Darüberhinaus besteht die Möglichkeit die erforderliche Eingabe-Daten für die zusammenhängende Menge der Unit Tests im Rahmen einer „Tear-Up“ Phase bereitzustellen oder die durchgeführten Änderungen mittels „Tear-Down“ Phase zurückzurollen.

4. Welche Unit Tests Frameworks gibt es?

Da Unit Tests meistens als White-Box Tests kodiert werden und die zu testenden SW Modulen direkt referenzieren / einbinden, werden sie typischerweise in der gleichen Programmiersprache und der gleichen Programmierumgebung implementiert.

Folgende Liste fasst die populärsten Unit Test Frameworks im Kontext einiger verbreiteten Programmiersprachen zusammen:

Programmiersprache Unit Test Frameworks
Java JUnit, TestNG, Spock
C# MS Unit, NUnit, xUnit
Java Script JSUnit, Jasmin, Karma
Python Pytest

Partnerangebot: Praxisnahe Schulungen

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

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

5. Einrichtung einer Entwicklungsumgebung

Für die Implementierung unserer Unit Tests verwenden wir nachfolgend die .NET Umgebung und das MS Unit Test Framework. Das Beispiel können Sie analog auch mit anderen Sprachen wie z.B. Java unter Verwendung von Eclipse ausprobieren.

Zur Einrichtung benötigen wir lediglich die aktuelle Entwicklungsumgebung Microsoft Visual Studio, die wir in der kostenlosen Community Version herunterladen und installieren.

Visual Studio 2017 – Community Editon – Windows

Nach der Installation können Sie noch abschließend die benötigten Arbeitsmodule selektieren.

Für dieses Tutorial wählen Sie bitte folgende Module aus:

  • „.NET Desktopentwicklung“
  • Plattformübergreifende .NET Core-Entwicklung
  • ASP.NET und Webentwicklung

6. Erstellung eines Unit Test Projektes

Best-Practise ist es die Unit Tests im gleichen Projekt wie Produktiv-Code anzulegen. Auf diese Weise können die Unit Tests noch effektiver die Entwickler während der Implementierung unterstützen, sind sozusagen stets zur Hand und können zum gleichen Zeitpunkt wie der Produktiv-Code refaktorisiert und angepasst werden. Außerdem unterliegen sie dann den gleichen Versionierungskriterien für den Quellcode.

Aus diesem Grund starten wir unsere Übung zuerst mit der Anlage einer gemeinsamen Projektsammlung (Solution) , in der wir zuerst das C#-Projekt für die Implementierung unserer „Multiplikator“ Applikation erstellen und anschließend ein MSUnit-Test Projekt hinzufügen, um die Applikation zu testen.

Neue C# Solution und Klassenbibliothek erstellen:

Visual Studio öffnen, in Datei-Menü auf „Neu“ -> „Projekt“ klicken.

Daraufhin erscheint ein Dialog, indem Sie den Typ des Projektes selektieren, das bei der Neuanlage der Solution automatisch mit-erstellt wird und für die Implementierung unserer Beispiel Applikation verwendet wird. Wählen Sie „Visual C#“ -> „.Net Core“ -> „Class Library (.Net Core)“ (C# Klassenbibliothek) aus.

Als Solution-Namen geben Sie „Multiplikator“ ein.

Neues Unit Test Projekt erstellen:

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

Daraufhin öffnet sich erneut ein Dialog, indem wir diesmal den Typ des Unit Test Projektes für unser Vorhaben selektieren.

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

Nach Abschluss dieser Aktionen haben wir eine Solution mit 2 Projekten, deren Struktur in Solution Explorer dargestellt wird.

  • Das Projekt „Multiplikator“ enthält eine vordefinierte Klasse „Class1.cs“, in der unsere Multiplikation implementiert werden kann.
  • Das Projekt „UnitTests“ enthält eine vordefinierte Klasse „UnitTest1.cs“, in der wir unsere UnitTests schreiben werden.

7. Erstellung der Unit Tests für das Beispiel-Szenario

Vorbereitung: Multiplikation ausimplementieren

Als nächstes übernehmen wir den entwicklungsseitigen Teil der Aufgabe und implementieren die in User Story definierte Anforderung.

Zu Beginn nennen wir die vordefinierte Datei „Class1.cs“ nach „Multiplikator.cs“ um, um die Code-Lesbarkeit zu verbessern.

  • Rechtsklick auf „Class1.cs“ -> Umbenennen -> „Multiplikator“ eingeben.
  • Visual Studio schlägt automatisch vor auch den Klassennamen und alle möglichen Verwendungen umzubenennen. Bestätigen Sie dies mit „Ja“

Mit Doppelklick auf die Datei „Multiplikator.cs“ öffnet sich die Klasse in VS Editor, in die wir folgende Implementierung der Multiplikation als Klassenmethode einfügen

public int Multiply(int operand1, int operand2)
{
    return operand1 * operand2;
}

Unit Tests erstellen

Nun können wir die vorgeschlagenen Testfälle für unser Beispiel-Szenario schreiben, die die neu implementierte Multiplikation testen.

Vorbereitung

1. Als Erstes nennen wir unsere vordefinierte Unit Test Klasse von „UnitTest1.cs“ nach „MultiplikatorTests.cs“ um und bestätigen die Aufforderung mit „Ja“.

2. Mit Doppelklick auf die Datei „MultiplikatorTests.cs“ öffnen sich die Klasse in VS Editor.

In der Klasse befindet sich bereits eine funktionsfähige Vorlage für einen Unit Test.

Gehen wir kurz die wichtigsten Bestandteile durch:

  • Die Datei inkludiert automatisch Microsoft.VisualStudio.TestTools.UnitTesting Namespace, im dem spezielle Funktionen für Unit Tests bereitstehen
  • Die Klasse wurde mit dem speziellen Attribut [TestClass] markiert.
  • Die leere Methode TestMethod1 wurde mit dem speziellem Attribut [TestMethod] markiert

Beide Attribute sind von zentraler Bedeutung, denn genau diese Attribute kennzeichnen die Unit Test Klasse als Sammlung ( =Testszenario) von Unit Test Methoden ( = Test Fälle), die von Visual Studio Compiler automatisch erkannt und zur Testdurchführung angeboten werden (dazu später mehr).

Hinweis: Andere UnitTest Frameworks können syntaktisch abweichende Markierungen erhalten, doch das Prinzip bleibt überall ähnlich.

Unit Test einfügen

3. Im nächsten Schritt nennen wir die Methode „TestMethod1“ in „TestPositiveMult“ um und fügen in diese Unit Test Methode folgenden C# Code ein, der unseren ersten Unit Test Fall implementiert und alle vorgestellten Bestandteie eines Unit Tests enhält. Damit sieht unser „TestPositiveMult“ – Unit Test etwa so aus:

       
        [TestMethod]
        public void TestPositiveMult()
        {
            // 1. Input
            int operand1 = 2;
            int opernad2 = 3;
            // Expected
            int expResult = 6;

            // 2. Call SuT
            Multiplikator.Multiplikator mult = new Multiplikator.Multiplikator();
            int result = mult.Multiply(operand1, opernad2);

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

Für die Validierung des Ergebnisses verwenden wir eine spezielle Klasse „Assert“, die uns MS Unit Test Framework bereitstellt.
Diese Klasse enthält zahlreiche spezielle Methoden um die Ergebnisse auf Gleichheit / Ungleichheit uvm. zu prüfen (s. Abb.)

Hinweis: Auch hier gilt: andere Unit Tests Frameworks können syntaktisch leicht abweichende Funktionen bereitstellen, doch das Prinzip bleibt bei allen Frameworks gleich.

Die Assertion prüft, ob das aktuelle Ergebnis mit dem erwarteten übereinstimmt und wirft im Fehlerfall eine Exception, wodurch der Testfall nach der Ausführung von dem Unit Test Framework automatisch als fehlgeschlagener Testfall markiert wird.

Code Abhängigkeiten hinzufügen

4. Unsere Implementierung des Unit Tests ist leider noch nicht ganz lauffähig. Das liegt daran, dass wir im Testfall die Multiply Methode unserer Software-Under-Test aufrufen, aber eben dieses SuT Modul noch gar nicht mit unserem Unit Test Projekt verknüpft haben.

Das holen wir gleich nach, indem wir mit Rechtsklick auf „Abhängigkeiten“ und dann auf „Referenz hinzufügen …“ klicken.

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

Dadurch kann unser UnitTest direkt auf die Implementierung im Projekt Multiplikator zugreifen.

Hier wird der White-box Charakter der Unit Tests deutlich. Sie referenzieren direkt das Software-under-Test Modul und interagieren damit durch die Instantiierung der Objekte der SuT-Klassen und den Aufruf ihrer Methoden.

Unit Test ausführen

5. Jetzt ist unser erster Unit Test lauffähig und wir können ihn sofort starten.

Klicken Sie mit Rechtsklick auf die Implementierung des Unit Tests und wählen sie aus dem Kontextmenü die Option „Run Tests“ aus.

Die Ergebnisse der (erfolgreichen) Testdurchführung erscheinen innerhalb einer Sekunde im „Test Explorer“ des Visual Studio (s. Abb.).

Dieses Fenster listet alle Testfälle innerhalb der aktuellen Projektmappe auf. Von hier aus können Sie jederzeit beliebige Tests erneut laufen lassen.

Weitere Unit Tests erstellen

Als Übungsaufgabe kopieren Sie die Implementierung des ersten Unit Tests und fügen Sie unterhalb des 1. Unit Tests analog 2 weitere Unit Tests hinzu, um die vorgeschlagenen 3 Testfälle unseres Beispiel-Szenarios abzudecken. Sie müssen lediglich die (fachlichen) Namen der Unit Test Methoden abändern und die Eingabeparameter sowie die erwarteten Ergebnisse jeweils anpassen.

Nach dem Abspeichern der Datei werden die neuen Unit Tests automatisch erkannt und in Test Explorer zur Durchführung angeboten.

Lassen Sie nun alle 3 Testfälle laufen. Die mit 8 ms angegebene Laufzeit verdeutlicht die mögliche, vorher beschriebene, hohe Geschwindigkeit der Unit Test Durchführung.

Schnelle Feedback Schleife:

Als ergänzende Aufgabe versuchen Sie einen Bug in die Implementierung unserer Demo Applikation in der Methode Multiply zu simulieren, indem Sie z.B. das Multiplikationszeichen durch eine Addition ersetzen. Wiederholen Sie die Unit Tests unmittelbar nach der Anpassung. Wie ändern sich die Testergebnisse? Wie lange hat es gedauert den Fehler nach der Änderung durch die Tests festzustellen?

Zusammenfassung

Glückwunsch, soeben haben Sie erfolgreich Ihre ersten Unit Tests implementiert und ausgeführt!

Die in diesem Teil erläuteten Konzepte und Beispiele bieten Ihnen eine Grundlage für den initialen Einstieg in das Thema „Unit Test Entwicklung“. Probieren Sie es am besten gleich am Beispiel Ihrer eigener Software-Komponenten aus! Fangen Sie zuerst mit einfachen Methoden an. Im Rahmen der praktischen Auseinandersetzung mit den Unit Tests werden Sie sich früher oder später auch mit fortgeschrittenen Themen beschäftigen müssen: Mocking, (Unit)Testbarkeit komplexer Methoden, Refactoring, Abhängigkeitsisolierung, kontinuierliche Test Coverage Messung und Einbindung in CI …

Im nächsten Teil der Serie beschäftigen wir uns mit den Integrationstests …

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.