Software Engineering 8. Unit Testing Franz-Josef Elmer, Universität Basel, HS 2016 Software Engineering: 8. Unit Testing 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 Zeit – Gebrochene Unit Tests werden sofort geflickt. Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 3 JUnit 4: Unit Testing Framework für Java – – – – – Unterstützung durch alle gängigen Java IDEs wie z.B. Eclipse. Ein Test ist eine mit @Test annotierte parameterlose public void deklarierte Methode einer Testklasse. Namenskonventionen: ● Testmethode: test<short description>() ● Testklasse: <Class to be tested>Test Ein Test Runner führt alle Testmethoden der Testklasse in unbestimmter Reihenfolge aus. Dabei wird jedesmal eine neue Instanz der Testklasse erzeugt. Ein Test ist erfolgreich wenn die Testmethode kein Throwable wirft. – Die Klasse Assert hat statische Methoden, die mit assert beginnen. Sie prüfen einen zu erwarteten Wert mit dem aktuellen Wert und werfen ein AssertionFailedError falls beide nicht übereinstimmen. – Die Methode Assert.fail wirft immer ein AssertionFailedError. Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 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 specifed temperature from Celsius to Fahrenheit. */ public double convertToFahrenheit(double temperature) { return 1.8 * temperature + 32; } } /** Converts the specifed temperature from Fahrenheit to Celsius. */ public double convertToCelcius(double temperature) { return (temperature - 32) / 1.8; } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 5 TemperatureConverterTest import org.junit.Assert; import org.junit.Test; public class TemperatureConverterTest { private static fnal double TOL = 1e-6; @Test public void testConvertToFahrenheit() { TemperatureConverter converter = new TemperatureConverter(); Assert.assertEquals(32, converter.convertToFahrenheit(0), TOL); Assert.assertEquals(86, converter.convertToFahrenheit(30), TOL); } } @Test public void testConvertToCelcius() { TemperatureConverter converter = new TemperatureConverter(); Assert.assertEquals(0, converter.convertToCelcius(32), TOL); Assert.assertEquals(30, converter.convertToCelcius(86), TOL); } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 6 Erwartete Exceptions testen – Es sollten auch Tests geschrieben werden, die das korrekte Verhalten auf Verletzung der Vorbedingungen (z.B. keine null als Methodenargument) überprüfen. – Test Code: ● ● Die zuerwartende Exception Klasse in Testannotation deklarieren. Beispiel: @Test(expected = NumberFormatException.class) public void testParseInvalidInteger() { Integer.parseInt("blabla"); } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 7 Beispiel: Stack import java.util.ArrayList; import java.util.List; public class Stack<E> { private fnal List<E> _stack = new ArrayList<E>(); /** * Pushes the specifed 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); } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 8 StackTest import org.junit.Assert; import org.junit.Test; public class StackTest { @Test public void testIsEmpty() { Stack<String> stack = new Stack<String>(); Assert.assertTrue(stack.isEmpty()); stack.push("hello"); Assert.assertFalse(stack.isEmpty()); stack.pop(); Assert.assertTrue(stack.isEmpty()); } @Test public void testPushPop() { Stack<String> stack = new Stack<String>(); stack.push("hello"); stack.push(null); stack.push("world"); Assert.assertEquals("world", stack.pop()); Assert.assertNull(stack.pop()); Assert.assertEquals("hello", stack.pop()); } @Test(expected = IllegalStateException.class) public void testPopFromEmptyStack() { new Stack<String>().pop(); } } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 9 Unit Testing Konventionen für Java – Die Testklasse ist im selben Paket wie die zu testende Klasse: ● – Vorteil: Testklasse hat Zugriff auf packageprotected Attribute und Methoden. Produktiver Code und Testcode sind in verschiedenen Verzeichnissen. ● Grund: Beim Build wird in der Regel nur der produktive Code benötigt. Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 10 Vorurteile – Tests schreiben ist minderwertiges Programmieren, dass kann man ruhig den Testern oder Junior-Programmierern überlassen. ● ● – Tests schreiben ist eine langweilige und stupide Tätigkeit. ● – Unit Tests schreiben ist so anspruchsvoll wie produktiven Code schreiben. Auch Testcode sollte qualitative guter Code sein. D.h. insbesondere seine Wartbarkeit sollte hoch sein. Unit Tests programmieren ist genau so kreative und macht genauso viel Spass wie produktiven Code schreiben. Unit Tests sind Zeitverschwendung. ● ● Unit Tests bilden ein Sicherheitsnetz, welches hilft älteren Code vor Fehlern zu schützen, die unbeabsichtigt durch neuen Code entstehen. Unit Tests verbessern das Design des produktiven Codes. Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 11 Die Kunst des Unit Testing I – Falls ein Test fehl schlägt, sollte zur Fehlersuche so viel Informationen gegeben werden, wie möglich. ● – Unit Testing hat Einfluss auf das Design. ● – Bespiel: Statt Assert.assertEquals(expectedList, actualList) besser Assert.assertEquals(expectedList.toString(), actualList.toString()). Der zu testende Code muss testbar sein, d.h. es ist möglich automatische Tests zu schreiben. Dabei hilft Modularisierung. 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 Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 12 Beispiel CommandLineTest import java.util.*; public class CommandLine { private fnal Set<String> _options; private fnal 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.unmodifableSet(options); _arguments = Collections.unmodifableList(arguments); } public List<String> getArguments() { return _arguments; } public Set<String> getOptions() { return _options; } } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 13 CommandLineTest import java.util.Arrays; import java.util.List; import org.junit.Assert; import org.junit.Test; public class CommandLineTest { @Test public void testWithoutOptions() { assertNoOptions("hello", "world"); } @Test public void testWithOptions() { assertOptionsAndArguments(Arrays.asList("b", "c"), Arrays.asList("hi"), "-b", "hi", "-c" ); } private void assertNoOptions(String... args) { assertOptionsAndArguments(Arrays.<String>asList(), Arrays.asList(args), args); } private void assertOptionsAndArguments(List<String> expectedOptions, List<String> expectedArguments, String... args) { CommandLine commandLine = new CommandLine(args); Assert.assertEquals(expectedOptions.toString(), commandLine.getOptions().toString()); Assert.assertEquals(expectedArguments.toString(), commandLine.getArguments().toString()); } } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 14 Die Kunst des Unit Testing II – – 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. Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 15 JUnit 4: Die Annotationen @Before und @After – Parameterlose public void Methoden einer Testklasse werden vor/nach jeder Ausführung einer Testmethode ausgeführt, falls sie mit @Before bzw. @After annotiert wurden. Traditionelle Name dieser Methoden sind: setUp() bzw. tearDown(). – Zweck dieser Annotationen: – ● ● 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 Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 16 Beispiel LineCounterTest – Die Klasse LineCounter zählt die Zeilen einer Textdatei: import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; public class LineCounter { public int countNumberOfLines(File fle) throws IOException { FileReader reader = null; try { reader = new FileReader(fle); BufferedReader bufferedReader = new BufferedReader(reader); int count = 0; while (bufferedReader.readLine() != null) { count++; } return count; } fnally { if (reader != null) { reader.close(); } } } } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 17 LineCounterTest – Die Testklasse muss eine Beispieldatei erzeugen und wieder wegräumen: import java.io.*; import org.junit.*; public class LineCounterTest { private static fnal File TEMP_FILE = new File("temp.txt"); @Before public void setUp() throws Exception { FileWriter writer = null; try { writer = new FileWriter(TEMP_FILE); writer.write("Hello\nworld"); } fnally { writer.close(); } } @After public void tearDown() throws Exception { TEMP_FILE.delete(); } @Test public void test() throws IOException { Assert.assertEquals(2, new LineCounter().countNumberOfLines(TEMP_FILE)); } } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 18 Die Kunst des Unit Testing III: Design – Eine Klasse soll nur abhängen von ● Interfaces, ● schon getesteten Klassen, ● Klassen, die nicht vom Zustand nichtkontrollierbarer Systeme (parallele Prozesse, Datenbanken, etc.) abhängen. – Ausnahme: Zyklische Abhängigkeiten zwischen wenigen Klassen. ● – Alle Klassen des Zyklus' werden gleichzeitig entwickelt und getestet. Alle Abhängigkeiten von nichtkontrollierbaren Systeme in Tests durch Attrappen (Dummies, Mocks) ersetzen. ● Solche Abhängigkeiten werden am besten durch Interfaces modelliert Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 19 Beispiel Logger import java.io.*; import java.text.*; import java.util.Date; public class Logger { private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); private final PrintWriter _writer; public Logger(Writer writer) { _writer = new PrintWriter(writer, true); } public void log(String message) { _writer.print("["); _writer.print(FORMAT.format(new Date(System.currentTimeMillis()))); _writer.print("] "); _writer.println(message); } } – Unit Testing Problem: Abhängig von der nichtkontrollier-baren Systemuhr. Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 20 Verbesserter Logger – Systemabhängigkeit wird hinter ein Interface versteckt: public interface TimeProvider { public long getCurrentTime(); } public class Logger { private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); private final PrintWriter _writer; private final TimeProvider _timeProvider; public Logger(TimeProvider timeProvider, Writer writer) { _timeProvider = timeProvider; _writer = new PrintWriter(writer, true); } public void log(String message) { _writer.print("["); _writer.print(FORMAT.format(new Date(_timeProvider.getCurrentTime()))); _writer.print("] "); _writer.println(message); } } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 21 LoggerTest – TimeProvider Attrappe: DummyTimeProvider public class LoggerTest extends TestCase { private static final String TIME_STAMP = "[" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(0)) + "]"; private static final class DummyTimeProvider implements TimeProvider { public long getCurrentTime() { return 0; } } public void test() { StringWriter stringWriter = new StringWriter(); Logger logger = new Logger(new DummyTimeProvider(), stringWriter); logger.log("Hello world!"); assertEquals(TIME_STAMP + " Hello world!\n", stringWriter.toString()); logger.log("Hi folks!"); assertEquals(TIME_STAMP + " Hello world!\n" + TIME_STAMP + " Hi folks!\n", stringWriter.toString()); } } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 22 Die Kunst des Unit Testing IV: Mocks – Dummy: ● Einfachste Implentierung, so dass Tests möglich sind. – Mock: ● ● ● – Mehr als ein Dummy. Zeichnet Methodenaufrufe auf oder Wirft Exception wenn unerwarteter Methodenaufruf. Zwei Stile: ● State-Based Testing: 1. Testfixture und Rückgabewerte des Mocks definieren. 2. Test durchführen: Mock zeichnet Methoden inklusive Parameter auf. 3. Aufzeichnung überprüfen. ● Interaction-Based Testing bzw. Endo Testing (Endo=Endoskop): 1. Testfixture definieren. 2. Rückgabewerte und Sequenz der Methodenaufrufe (inkl. erwartete Parameter) des Mocks definieren. 3. Test durchführen: Exceptions werfen bei unerwartete Methodenaufrufen oder unerwartetem Parameterwerten. 4. Überprüfen, ob alle Methoden auch wirklich aufgerufen wurden. Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 23 Beispiel Parser public class Parser { /** * Parses the specified text. Parsing events are handled by the specified handler. * {@link ParseHandler#openingBracketFound()} * and {@link ParseHandler#closingBracketFound()} are invoked when '(' and ')', * resp., are detected. * The text before, between, and after the brackets is splitted into elements. * The delimiters are any space sequences (i.e. one or more space characters). * For each text element {@link ParseHandler#elementFound(String)} is invoked. * * @param text Text to be parsed. * @param handler Handler processing parsing events. */ public void parse(String text, ParseHandler handler) { // TODO implementation } } public interface ParseHandler { public void openingBracketFound(); public void closingBracketFound(); public void elementFound(String element); } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 24 ParserTest im Stil State-Based Testing public class ParserTest extends TestCase { private static final Object OPENING = new Object(); private static final Object CLOSING = new Object(); private static final class MockParserHandler implements ParseHandler { List _events = new ArrayList(); public void openingBracketFound() { _events.add(OPENING); } public void elementFound(String element) { _events.add(element); } public void closingBracketFound() { _events.add(CLOSING); } } public void testWithoutBrackets() { check(new Object[0], " "); check(new Object[] {"Hello"}, " Hello "); check(new Object[] {"Hello", "world"}, "Hello world"); } public void testWithBrackets() { check(new Object[] {"3", "*", OPENING, "a", "-", "b", CLOSING}, "3 * (a - b)"); } private void check(Object[] expectedEvents, String text) { MockParserHandler parserHandler = new MockParserHandler(); new Parser().parse(text, parserHandler); assertEquals(expectedEvents, parserHandler._events.toArray()); } } Universität Basel, HS 2016 © Franz-Josef Elmer 2016 Software Engineering: 8. Unit Testing 25 Refactoring in IDEs Links Martin Fowler: UnitTest (2014) http://martinfowler.com/bliki/UnitTest.html Universität Basel, HS 2016 © Franz-Josef Elmer 2016