Software Engineering 4. Unit Testing und Refactoring Franz-Josef Elmer, Universität Basel, HS 2007
Software Engineering: 4. Unit Testing und Refactoring 2 Unit Testing Unit Test: Automatischer Test welcher eine Einheit (z.b. Modul, Klasse, Komponente etc.) testet. Unit Testing: Erstellen, Verwalten und Ausführen aller Unit Tests. Unit Tests werden gleichzeitig mit dem produktiven Code geschrieben. Motto: Code a little, test a little. Produktiver Code Test Code Gebrochene Unit Tests werden sofort geflickt. Unit Testing ist das Fundament aller agilen Softwareentwicklungsmethodologien. Zeit
Software Engineering: 4. Unit Testing und Refactoring 3 JUnit: Unit Testing Framework für Java Ein Test ist eine Methode einer Testklasse (Subklasse von TestCase) mit einer der beiden folgenden Signaturen: public void testxxx() public void testxxx() throws YYY Dabei ist XXX beliebig und YYY ist eine beliebige Subklasse von Throwable. Ein Test Runner führt alle Testmethoden der Test Klasse aus (Reihenfolge ist unbestimmt). Ein Test ist erfolgreich wenn die Testmethode kein Throwable wirft. Die Methode fail wirft ein AssertionFailedError. Alle Methoden von TestCase, die mit assert beginnen, prüfen einen erwarteten Wert mit dem aktuellen Wert und werfen ein AssertionFailedError.
Software Engineering: 4. Unit Testing und Refactoring 4 Beispiel TemperaturConverterTest /** * Temperature converter between Fahrenheit and Celcius. * Conversion is based on the formula * <pre>fahrenheit = 9 * Celcius / 5 + 32</pre> */ public class TemperatureConverter { /** Converts the specified temperature from Celsius to Fahrenheit. */ public double converttofahrenheit(double temperature){ return 1.8 * temperature + 32; /** Converts the specified temperature from Fahrenheit to Celsius. */ public double converttocelcius(double temperature) { return (temperature 32) / 1.8;
Software Engineering: 4. Unit Testing und Refactoring 5 TemperatureConverterTest import junit.framework.testcase; public class TemperatureConverterTest extends TestCase { private static final double TOL = 1e 6; public void testconverttofahrenheit() { TemperatureConverter converter = new TemperatureConverter(); assertequals(32, converter.converttofahrenheit(0), TOL); assertequals(86, converter.converttofahrenheit(30), TOL); public void testconverttocelcius() { TemperatureConverter converter = new TemperatureConverter(); assertequals(0, converter.converttocelcius(32), TOL); assertequals(30, converter.converttocelcius(86), TOL); public static void main(string[] args) { junit.textui.testrunner.run(temperatureconvertertest.class);
Software Engineering: 4. Unit Testing und Refactoring 6 Test Setup und Tear Down TestCase hat zwei überschreibbare Methoden, die vor bzw. nach jedem Test aufgerufen werden: protected void setup() throws Exception protected void teardown() throws Exception TestRunner «create» YYYTest setname( testxxx ) runbare() setup() testxxx() teardown()
Software Engineering: 4. Unit Testing und Refactoring 7 Erwartete Exceptions testen Es sollten auch Tests geschrieben werden, die das korrekte Verhalten auf Verletzung der Vorbedingungen überprüfen. Test Code: Typisch: Verletzung der Vorbedingungen Exception Verletzung in try catch Klammer provizieren. Vor dem catch Statement wird fail() Methode aufgerufen. Im catch Teil kann die Exception weiter untersucht werden. Beispiel: public void testparseinvalidinteger() { try { Integer.parseInt("blabla"); fail("numberformatexception expected"); catch (NumberFormatException e) { asserttrue(e.getmessage().indexof("blabla") >= 0);
Software Engineering: 4. Unit Testing und Refactoring 8 Beispiel StackTest import java.util.*; public class Stack<E> { private final List<E> _stack = new ArrayList<E>(); /** Pushs the specified element onto the stack. * @param element Any object of type E. <code>null</code> is allowed. */ public void push(e element) { _stack.add(element); /** Returns <code>true</code> if the stack is empty. */ public boolean isempty() { return _stack.isempty(); /** Removes and returns the element on the top of the stack. * @throws IllegalStateException if the stack is empty. */ public E pop() { if (isempty()) { throw new IllegalStateException("Can not pop from an empty stack."); return _stack.remove(_stack.size() 1);
Software Engineering: 4. Unit Testing und Refactoring 9 StackTest import junit.framework.testcase; public class StackTest extends TestCase { public void testisempty() { Stack<String> stack = new Stack<String>(); asserttrue(stack.isempty()); stack.push("hello"); assertfalse(stack.isempty()); stack.pop(); asserttrue(stack.isempty()); public void testpushpop() { Stack<String> stack = new Stack<String>(); stack.push("hello"); stack.push(null); stack.push("world"); assertequals("world", stack.pop()); assertnull(stack.pop()); assertequals("hello", stack.pop()); public void testpopfromemptystack() { Stack<String> stack = new Stack<String>(); try { stack.pop(); fail("illegalstateexception expected"); catch (IllegalStateException e) {
Software Engineering: 4. Unit Testing und Refactoring 10 Test Suite Test Cases werden in Test Suiten zusammengefasst. TestSuite ist Subklasse von TestCase hierarchische Organisation aller Tests. Beispiel: import junit.framework.*; public class AllTests { public static Test suite() { TestSuite suite = new TestSuite("Test for junitexample"); suite.addtestsuite(temperatureconvertertest.class); suite.addtestsuite(stacktest.class); return suite; public static void main(string[] args) { junit.textui.testrunner.run(alltests.suite());
Software Engineering: 4. Unit Testing und Refactoring 11 JUnit Konventionen Der Name einer Testklasse beginnt mit dem Name der zu testenden Klasse und endet mit Test. Beispiel: Klasse: Stack, Testklasse: StackTest Die Testklasse ist im selben Paket wie die zutestende Klasse: Vorteil: Testklasse hat Zugriff auf package-protected Attribute und Methoden. Alle Klassen mit den Test Suiten heissen AllTests. Enhält Testklasse des selben Pakets und Test Suiten der Unterpakete. Beispiel: public static Test suite() { TestSuite suite= new TestSuite(); suite.addtestsuite(jcckit.svgplottertest.class); suite.addtest(jcckit.transformation.alltests.suite()); suite.addtest(jcckit.util.alltests.suite()); return suite;
Software Engineering: 4. Unit Testing und Refactoring 12 Vorurteile Tests schreiben ist minderwertiges Programmieren, dass kann man ruhig den Testern oder Junior- Programmieren überlassen. Unit Tests schreiben ist mindestens so anspruchsvoll wie produktiven Code schreiben. Alle Designprinzipien sollten auch beim Schreiben von Testcode einfliessen. Tests schreiben ist eine langweilige und stupide Tätigkeit. Unit Tests programmieren ist genau so kreative und macht genauso viel Spass wie produktiven Code schreiben. Unit Tests sind Zeitverschwendung. Unit Tests sind ein Sicherheitsnetz unter dem Trapezakt Softwareentwicklung. Unit Tests verbessern das Design des produktiven Codes.
Software Engineering: 4. Unit Testing und Refactoring 13 Die Kunst des Unit Testing Unit Testing hat Einfluss auf das Design. Der zu testende Code muss testbar sein, d.h. es ist möglich automatische Tests zu schreiben. Wenn ein Bug gefunden wurde: 1.Finde die Ursache. 2.Schreibe einen Unit Test, der wegen des Bugs scheitert. 3.Fixe den Bug bis der Unit Test nicht mehr fehlschlägt. Unit Tests sind Test Cases und sollten deshalb so leicht lesbar sein wie manuelle Test Cases. Keine Verzweigungen in der Test Methode. Klare Trennung von Testdaten und Testcode. Komplexere Überprüfungen in eigene assert Methoden auslagern. Beispiel: CommandLineTest
Software Engineering: 4. Unit Testing und Refactoring 14 Beispiel CommandLineTest import java.util.*; public class CommandLine { private final Set<String> _options; private final List<String> _arguments; public CommandLine(String[] args) { Set<String> options = new HashSet<String>(); List<String> arguments = new ArrayList<String>(); for (String arg : args) { if (arg.startswith(" ")) { options.add(arg.substring(1)); else { arguments.add(arg); _options = Collections.unmodifiableSet(options); _arguments = Collections.unmodifiableList(arguments); public List<String> getarguments() { return _arguments; public Set<String> getoptions() { return _options;
Software Engineering: 4. Unit Testing und Refactoring 15 CommandLineTest public class CommandLineTest extends TestCase { public void testwithoutoptions() { checknooptions(new String[] {"hello"); checknooptions(new String[] {"hello", "world"); public void testwithoptions() { check(new String[] {"b", "c", new String[] {"hi", new String[] {" b", "hi", " c"); private void checknooptions(string[] args) { check(new String[0], args, args); private void check(string[] expectedoptions, String[] expectedarguments, String[] args) { CommandLine commandline = new CommandLine(args); assertequals(expectedoptions, commandline.getoptions().toarray()); assertequals(expectedarguments, commandline.getarguments().toarray()); private void assertequals(object[] array1, Object[] array2) { for (int i = 0, n = Math.min(array1.length, array2.length); i < n; i++) { assertequals("array[" + i + "]", array1[i], array2[i]); assertequals(array1.length, array2.length);
Software Engineering: 4. Unit Testing und Refactoring 16 Die Kunst des Unit Testing Unit Tests müssen reproduzierbar sein. Dazu braucht es eine wohldefinierte Testumgebung (Testfixture). Ein Unit Test sollte den Zustand seiner Umgebung vor dem Test wieder herstellen. Vermeidet Seiteneffekte. Tests sind reproduzierbar unabhängig der Reihenfolge ihrer Ausführung. Problem: Statische Attribute von Klassen, die ihren Zustand ändern.
Software Engineering: 4. Unit Testing und Refactoring 17 Der Sinn von setup() und teardown() Wiederverwendung von Code, den jede Testmethode vor bzw. nach dem eigentlichen Test ausführen muss. Bereitstellung bzw. Freigabe von externen Resourcen. Z.B.: Temporäre Dateien, Datenbankverbindungen. Erzeugung bzw. Entfernung von Testfixtures. Z.B.: setup() spielt Testdaten in eine Datenbank ein und teardown() löscht diese wieder. Beispiel: LineCounterTest
Software Engineering: 4. Unit Testing und Refactoring 18 Beispiel LineCounterTest Die Klasse LineCounter zählt die Zeilen einer Textdatei: import java.io.*; public class LineCounter { public int countnumberoflines(file file) throws IOException { FileReader reader = null; try { reader = new FileReader(file); BufferedReader bufferedreader = new BufferedReader(reader); int count = 0; while (bufferedreader.readline()!= null) { count++; return count; finally { if (reader!= null) { reader.close();
Software Engineering: 4. Unit Testing und Refactoring 19 LineCounterTest Die Testklasse muss eine Beispieldatei erzeugen und wieder wegräumen: public class LineCounterTest extends TestCase { private static final String TEMP_FILE = "temp.txt"; protected void setup() throws Exception { super.setup(); FileWriter writer = null; try { writer = new FileWriter(TEMP_FILE); writer.write("hello\nworld"); finally { writer.close(); protected void teardown() throws Exception { super.teardown(); new File(TEMP_FILE).delete(); public void test() throws IOException { assertequals(2, new LineCounter().countNumberOfLines(new File(TEMP_FILE)));
Software Engineering: 4. Unit Testing und Refactoring 20 Refactoring Definition: Verbesserung des Codes ohne Änderung des Verhaltens. Allgemeine Erfahrung: Code Qualität Code Qualität Zahl der Features Zeit Zahl der Features Refactoring Phasen Zeit
Software Engineering: 4. Unit Testing und Refactoring 21 Refactoring Refactoring ist riskant, deshalb Risiko mindern durch gute Unit Test Abdeckung Immer in kleinen Schritten: Ein Refactoring Schritt Testen Nächster Refactoring Schritt Testen usw. Häufiger Wechsel zwischen Implementation eines neuen Features und Refactoring Neue Features Refactoring Zeit
Software Engineering: 4. Unit Testing und Refactoring 22 Ziele des Refactoring Lesbarkeit des Codes erhöhen. Refactoring kann parallel zu einem Code Review erfolgen. Design verbessern (sogenannte Bad Smells beseitigen). Code so umbauen, dass es möglich ist, Unit Tests zu schreiben. Code so vorbereiten, dass neue Features implementiert werden können.
Software Engineering: 4. Unit Testing und Refactoring 23 Reengineering Bestehende (Alt)Software (engl. legacy software) fit machen für Erweiterungen neue Umgebung Performance Optimierungen etc. Zwei Strategien: Viele kleine Schritte: Refactoring im Grossen. Zwei grosse Schritte: 1.Reverse Engineering: Von der Implementierung zurück zur Abstraktion und dem Design 2.Forward Engineering: Redesign und neue Implementierung
Software Engineering: 4. Unit Testing und Refactoring 24 Bad Smell Bad Smells sind Stellen im Code die stinken, d.h. es gibt Probleme wegen Verstoss gegen Design Prinzipien schlecht lesbarem Code. Die wichtigsten Bad Smells: Duplizierter Code Hoher Wartungsaufwand da Änderungen überall nachgeführt werden müssen. Lange Methode Schwierig zu verstehen Schlechte Wiederverwendbarkeit Ursache von Code Duplikationen Grosse Klasse Oft schlechte Wiederverwendbarkeit da die Klasse viele verschiedene Dinge machte Ursache von Code Duplikationen Lange Parameter Liste Schwierig zu lesen Switch Statements bzw. if-else-if Ketten Möglicherweise unflexibel für Erweiterungen Gleichartige Switch Statements: Code Duplikationen
Software Engineering: 4. Unit Testing und Refactoring 25 Refactoring Katalog Katalog der verschiedenen möglichen Refactorings. Wichtigster Katalog für objekt-orientiertes Refactoring: Format: Martin Fowler et al.: Refactoring Improving the Design of Existing Code. Addison-Wesley (2000) Name Zusammenfassung: Kurze Problembeschreibung Kurze Lösungsbeschreibung Einfaches Vorher-Nachher Beispiel Motivation: Beschreibung warum und warum nicht. Mechanik: Schritt-für-Schritt Anleitung. Beispiele: Grössere(s) Beispiel(e).
Software Engineering: 4. Unit Testing und Refactoring 26 Methode extrahieren (Extract Method) Ein Codefragment kann zusammengefasst werden. Setze das Fragment in eine Methode, deren Name den Zweck bezeichnet. void printowing(double amount) { printbanner(); //print details System.out.println ("name: " + _name); System.out.println ("amount " + amount); void printowing(double amount) { printbanner(); printdetails(amount); void printdetails(double amount) { System.out.println ("name: " + _name); System.out.println ("amount " + amount);
Software Engineering: 4. Unit Testing und Refactoring 27 Methode extrahieren (Extract Method) Motivation: Verbesserung der Lesbarkeit. Indiz: Kommentarzeile vor dem Fragment das extrahiert werden kann. Codeduplikation: Verschiedene Codefragmente tun (fast) dasselbe. Name der neuen Methode sollte selbstsprechend sein. Nicht möglich wenn mehr als eine lokale Variable im Fragment geändert wird aber danach noch benötigt wird. Mechanik: 1.Neue Methode erzeugen. 2.Zu extrahierendes Codefragment dort hin kopieren. 3.All lokalen Variablen, die gelesen werden, werden Methodenparameter. 4.Die lokale Variable, die geändert wird, wird der Rückgabewert. 5.Compiler laufen lassen und Fehler beheben. 6.Zu extrahierenden Code entfernen und durch Aufruf der neuen Methode ersetzen. 7.Compiler laufen lassen und Fehler beheben. 8.Unit Test der Klasse (besser alle Unit Tests) ausführen.
Software Engineering: 4. Unit Testing und Refactoring 28 Methode extrahieren (Extract Method) Beispiel: void handlearguments(string[] args) { try { _width = Integer.parseInt(args[0]); catch (Exception e) { _width = DEFAULT_WIDTH; try { _height = Integer.parseInt(args[1]); catch (Exception e) { _height = DEFAULT_HEIGHT; Refactored: void handlearguments(string[] args) { _width = getintegerargument(args, 0, DEFAULT_WIDTH); _height = getintegerargument(args, 1, DEFAULT_HEIGHT); int getintegerargument(string[] args, int index, int defaultvalue) { try { return Integer.parseInt(args[index]); catch (Exception e) { return defaultvalue;
Software Engineering: 4. Unit Testing und Refactoring 29 Beschreibende Variable einführen (Introduce Explaining Variable) Es gibt einen komplizierten Ausdruck. Setze den Ausdruck (oder Teile) in eine lokale Variable deren Name den Zweck erklärt. if ( (platform.touppercase().indexof("mac") > 1) && (browser.touppercase().indexof("ie") > 1) && wasinitialized() && resize > 0 ) { // do something final boolean ismacos = platform.touppercase().indexof("mac") > 1; final boolean isiebrowser = browser.touppercase().indexof("ie") > 1; final boolean wasresized = resize > 0; if (ismacos && isiebrowser && wasinitialized() && wasresized) { // do something
Software Engineering: 4. Unit Testing und Refactoring 30 Methode umbenennen (Rename Method) Der Name einer Methode macht ihre Absicht nicht deutlich. Ändere den Name der Methode. Customer +getinvcdlimit() Customer +getinvoicablecreditlimit()
Software Engineering: 4. Unit Testing und Refactoring 31 Methode hochziehen (Pull Up Method) Es gibt Methoden mit identischen Ergebnissen in den Unterklassen. Verschiebe die Methoden in die Oberklasse. Superclass Superclass +method() SubclassA +method() SubclassB +method() SubclassA SubclassB
Software Engineering: 4. Unit Testing und Refactoring 32 Template Method einsetzen (Form Template Method) Zwei Methoden in Unterklassen führen ähnliche aber im Detail verschiedene Schritte in derselben Reihenfolge aus. Extrahiere die Schritte in Methoden gleicher Signatur, so dass die ursprünglichen Methoden identisch werden. Verschiebe diese dann in die Oberklasse.
Software Engineering: 4. Unit Testing und Refactoring 33 Form Template Method Site ResidentialSite getbillableamount():double LifelineSite getbillableamount():double double base = _units * _rate; double tax = base * Site.TAX_RATE; return base + tax; double base = _units * _rate * 0.5; double tax = base * Site.TAX_RATE * 0.2; return base + tax; Site getbillableamount():double getbaseamount():double gettaxamount():double return getbaseamount() + gettaxamount(); ResidentialSite getbaseamount():double gettaxamount():double LifelineSite getbaseamount():double gettaxamount():double
Software Engineering: 4. Unit Testing und Refactoring 34 Parameterobjekt einführen (Introduce Parameter Object) Ein Gruppe von Parametern die zusammengehören. Ersetze diese Parameter durch ein Objekt. Customer +amountinvoicedin(start:date, end:date) +amountreceivedin(start:date, end:date) +amountoverduein(start:date, end:date) Customer +amountinvoicedin(range:daterange) +amountreceivedin(range:daterange) +amountoverduein(range:daterange)
Software Engineering: 4. Unit Testing und Refactoring 35 Refactoring in IDEs Viele integerierte Entwicklungsumgebungen (IDEs) unterstützen automatisches Refactoring. Vorteile: Einfach und schnell Zuverlässig High-level source code editing Nachteile: Unzuverlässig Unerwartete Seiteneffekte
Software Engineering: 4. Unit Testing und Refactoring 36 Links JUnit Home Page: http://www.junit.org/ Refactoring Home Page http://www.refactoring.com/ Joel Spolsky: Rub a dub dub (2002) Eine Geschichte von erfolgreichem Refactoring (nicht in Java) http://www.joelonsoftware.com/articles/fog0000000348.html David Gallardo: Refactoring for everyone - How and why to use Eclipse's automated refactoring features (2003) http://www-128.ibm.com/developerworks/library/os-ecref/