Unit Tests in der Testgetriebenen Entwicklung Reduzierung des Testanteils am Gesamtsoftwareentwicklungsaufwand -- neue Strategien und Tools -- Hauptseminar Systemorientierte Informatik Christian Krauß
Teil I Testen Warum? Was? Wann? Wie?
Warum? Testen Aufspüren möglichst vieler (aller) Fehler Überprüfen von (nicht)funktionalen Anforderungen Besitzt System geforderte Funktionalität? Erfüllt es Zeitanforderungen? Läuft es auf der Zielplattform? Verhinderung von Seiteneffekten Was? Code innerhalb eines Blocks Methoden Klassen funktional zusammengehörende Einheiten Gesamtsystem
Testen Wann? Wie? (am Beispiel Unit Test) parallel zur Programmierung White-Box nach Fertigstellung eines Moduls Black-Box interne Struktur bekannt (evtl. für Messungen bzw. Stimulation zugängig) Strukturtest nur Schnittstellen des Systems sind zugänglich (Ein-/Ausgänge) Funktionstest
Teil II + Direktiven Testgetriebener Entwicklung + Entwicklungszyklus + JUnit
Testen Erkenntnisse und Ideen Erfahrungen - Testen ist häufig: - die letzte Phase in wasserfallartigen Projekten - ein der Programmierung untergeordneter Prozess - das Erste, was über Bord geht, wenn es eng wird - nur ad hoc, selten systematisch. Probleme: - Fehler werden zu spät erkannt - Entwickler erhalten wenig Feedback über Qualität des Codes - Seiteneffekte nach Änderung von Design bzw. Implement. - Code ist unzureichend testbar
Testen Erkenntnisse und Ideen Anforderungen an effektive Teststrategien, Testen sollte: - möglichst zeitnah zur Programmierung erfolgen schnelles Feedback über Softwarequalität so häufig wie das Kompilieren ausführen - automatisiert wiederholbar sein - pragmatisch sein (kein Beweis der Fehlerfreiheit) - mehr bringen als kosten
Direktiven - Übersicht Die Testgetriebene Entwicklung definiert folgende Direktiven: 1. Motivieren Sie jede Änderung des Programmverhaltens zuvor durch einen automatisierten Test. 2. Bringen Sie Ihren Code immer in die einfachste Form (Refactoring). 3. Integrieren Sie Ihren Code so häufig wie nötig. Die Direktiven stehen im engen Zusammenhang mit der iterativinkrementellen Entwicklung von Software (Anwendung: XP)
Direktive I Motivieren Sie jede Änderung des Programmverhaltens zuvor durch einen automatisierten Test. Test beschreibt Anforderung Test schlägt fehl Funktionalität implementieren Idee: Test First Programming Test treibt Entwicklung voran - Test sichert Erfüllung der Anforderung - nur das programmieren, was erforderlich ist - jederzeit Kontrolle über System behalten - schnelles Feedback (Erfolgserlebnisse) Testerfolg
Direktive II: Refactoring Bringen Sie Ihren Code immer in die einfachste Form. Never change a running system? Änderungen führen zu Seiteneffekten werden auf Mindestmaß beschränkt Alterung der Software Refactoring: - ständige Anpassung der Struktur (Funktionalität unverändert) - zielorientiert (nur im Zusammenhang mit nächster Anforderung) - in kleine Refactorings zerlegen - Komponententests verhindern Seiteneffekte (Voraussetzung!!!)
Direktive III: Integration Vorteile häufiger Integration: - Änderungen anderen Entwicklern zugängig machen - kleinere Refactorings (Merge-Aufwand verringert) - Konflikte aufdecken, wenn sie klein sind Integration verpflichtet Entwickler: alle Tests des integrierten Systems laufen (Fehlschlag: Änderungen rückgängig machen) lauffähige Version jederzeit verfügbar integriertes System erfüllt mehr und mehr Akzeptanztests Prinzip der schnellen Feedbacks (bzgl. Gesamtsystem)
Komponententests mit JUnit - Tool (Framework) zur Durchführung von automatisierten Unit Tests - Sprache: Java - aktuelle Version: 3.8.1 - Autoren: Kent Beck, Erich Gamma - Lizenz: Open Source, IBM CPL - Test: Softwareeinheiten in Isolation zu anderen Einheiten (Methoden, Klassen) - Testfälle in Java kodiert (in Testklassen zusammengefasst) - Integration in IDEs (z.b. als Eclipse-Plugin) - Unterstützung der iterativ-inkrementellen testgetriebenen Entwicklung
Iterativ-inkrementelle Entwicklung Grundprinzipien: Entwicklungszyklus kleine Schritte Vorgehensweise: Vorbereitung: Anforderungen an Softwareeinheit definieren ToDo-Liste 1. zu realisierende Anforderung auswählen 2. Anforderungen in Testklasse integrieren (Testfall) Daumenregel: je Klasse eine Testklasse nur ein Test zu jedem Zeitpunkt (jeder Test muss seinen Wert beweisen) 3. Testklasse in Testsuite integrieren (einmalig)
Iterativ-inkrementelle Entwicklung Vorgehensweise (Fortsetzung): 4. Kompilieren Fehlschlag Feedback: neue Klasse/Methode tatsächlich erforderlich 5. Klasse mit leeren Operationsrümpfen versehen 6. Kompilieren Erfolg, Test Fehlschlag Feedback: Testklasse korrekt in Suite integriert 7. Methode implementieren nur soviel Code, dass Test erfüllt wird 8. Test Erfolg/Misserfolg der gerade geschriebene Code macht den Unterschied Rückschritte zur letzten funktionierenden Version klein 9. Refactoring durch Tests absichern Seiteneffekte verhindern
Teil III + Entwicklungszyklus am Beispiel + Testen mit JUnit
Beispiel: Klasse Euro Klasse Euro: - stellt Wertbeträge dar - Operation auf Euro-Objekt neues Euro-Objekt (Wertsemantik) Anforderungen definieren: - Konstruktur mit Betragsparameter - Addieren von Beträgen - Runden auf Cent genau - 100 Cent = 1 Euro
Testklasse EuroTest import junit.framework.*; public class EuroTest1 extends TestCase { private Euro1 two; protected void setup() { two = new Euro1(2.00); } public void testamount() { asserttrue(2.00 == two.getamount()); } public static void main(string[] args) { junit.swingui.testrunner.run(eurotest1.class); } }
Analyse der Testklasse - Paket: junit.framework - Testklassen sind von Klasse TestCase abgeleitet - Erzeugung eines Test-Fixture: Methode setup() - Testfälle: Methoden mit Signatur public void test...() - Annahmen/Forderungen/Zusicherungen: assert...() (Klasse Assert) asserttrue(), assertfalse() assertnull(), assertnotnull() assertequals(expected, actual) Vielzahl von Datentypen: Object, String, int,... assertsame(), assertnotsame () (Referenzvgl.) - Darstellung: GUI (Swing), weitere: GUI (AWT), Konsole
Klasse Euro public class Euro1 { private double amount; public Euro1(double amount) { //this.amount = amount; } public double getamount() { //return this.amount; return 0.0; } }
Lebenszyklus eines Testfalls
Lebenszyklus eines Testfalls Grundsatz beim Testlauf: jeder Testfall muss isoliert ausgeführt werden Vorgehensweise von JUnit: - Erzeugung: für jeden Testfall ein eigenes Objekt - Testlauf: Testfälle getrennt ausgeführt (Reihenfolge undef.) abhängige Testfälle in eine test...()-methode integrieren Ausführung eines Testfalls: 1. setup(): Test-Fixture erzeugen (Ressourcen binden) 2. test...(): Test durchführen 3. teardown(): Ressourcen freigeben
TestSuite import junit.framework.*; public class AllTests { public static Test suite() { TestSuite suite = new TestSuite(); suite.addtestsuite(customertest.class); suite.addtestsuite(eurotest.class); suite.addtestsuite(movietest.class); return suite; } public static void main(string[] args) { junit.swingui.testrunner.run(alltests.class); } }
Teil IV Zusammenfassung / Bewertung
Einsatz Testgetriebener Entwicklung Ansatz iterativ-inkrementeller Vorgehensmodelle: - reines Nachdenken über Architektur schwierig - Softwareentwicklung ist Lernprozess - Schwächen am laufenden System entdecken Einsatz testgetriebener Entwicklung ist Grundvoraussetzung Grundsatz Make it run Make it right Make it fast Tests beschreiben fkt. Anforderungen und sichern deren Erfüllung Refactoring vereinfacht Design Tests verhindern Seiteneffekte
Bewertung Qualitätssteigerung - Programmierfehler frühzeitig entdeckt - Entwürfe einfacher Einfach zu beschreibende automatische Tests Testklassen unterstützen Dokumentation des Systems - exemplarische Spezifikation Dokumentation des Testens - Sicherheit/Erweiterungsgrundlage für Anwendungsentwickler (Bibliotheken, Frameworks) Testdisziplin bei jedem Mitarbeiter - Aussetzen von Tests bedingt riesigen Aufwand, um Testabdeckung wieder zu erreichen Erhöhter Aufwand bei Implementierung