Software Engineering 8. Unit Testing

Werbung
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
Herunterladen