A power workshop during JAX 2007 on advanced techniques of test-driven development. It deals with acceptance tests using FIT as well as with mock objects, GUI testing and Groovy as a testing language for Java.
2. Agenda
• Testgetriebene Entwicklung im Überblick
• Akzeptanztests mit FIT
• Unit Tests mit JUnit/Mockobjekte mit EasyMock
• Legacy Code und Code Coverage
• Testen von GUIs und Web-Applikationen
• Testen mit Skriptsprachen
• Diskussion
3. Motivation
• Software hat Geschäftswert durch zwei Qualitäten
• Funktionale Qualität
(Funktionalität, Fehlerfreiheit)
• Strukturelle Qualität
(Design/Codestruktur für Weiterentwicklung)
• Hinzufügen neuer Funktionalität
gefährdet funktionale und strukturelle Qualität
• Verbesserung der strukturellen Qualität
gefährdet die funktionale Qualität
4. Testgetriebene Entwicklung
• Testgetriebene Programmierung:
Motiviere jede Verhaltensänderung am Code
durch einen automatisierten Test
(ständige Absicherung der funktionalen Qualität)
• Refactoring:
Vereinfache das Code-Design soweit wie möglich
(funktionale Qualität ist durch Tests abgesichert)
• (Häufige Integration:
Integriere den Code so häufig wie nötig)
5. Entwickler schreiben Unit Tests
• testen Komponenten des Systems in Isolation
• geben uns konkretes Feedback
• ermöglichen sichere Änderungen
• sichern Erhalt der vorhandenen Funktionalität
• müssen bei jeder Integration zu 100% laufen
• können funktionale Tests auf Systemebene
nicht ersetzen!
6. Kunden spezifizieren Akzeptanztests
• testen das System überwiegend als Ganzes
• geben unseren Kunden Vertrauen
in die gelieferte Software
• klären die Anforderungen
frühzeitig an konkreten Beispielen
• machen den Projektfortschritt sichtbar
• müssen vom Kunden erstellt und gepflegt
werden können
• Unsere Aufgabe ist es, den entsprechenden
Rahmen für ihre Automatisierung zu schaen!
7. Akzeptanztests mit FIT
• FIT: Framework for Integrated Test
• Zum Schreiben und Ausführen automatischer
Akzeptanztests
• Testdaten werden tabellarisch erstellt
(in HTML, mit Excel oder im Wiki)
• Anbindung ans System in Java
• Portierung für aktuelle Sprachen verfügbar
• http://fit.c2.com
8. Drei Fixture-Klassen
• ColumnFixture testet Ein-/Ausgabewertemengen.
• ActionFixture spielt ein Benutzerszenario durch
und ist deshalb gut für GUI-orientierte Tests
geeignet.
• RowFixture prüft eine Ergebnismenge
von Objekten mit ihren Attributen.
12. Action Fixture (2)
import fit.ActionFixture;
public class AccountAdministrationFixture
extends ActionFixture {
public void newAccount() {}
public void customerName(String name) {}
public String accountNumber() {
return 0;
}
public void deposit(double money)
throws AccountException {
}
public double balance() {
return 0;
}
}
13. Row Fixture (1)
public class AccountListingItem {
public String accountNumber;
public double balance() {
return 0;
}
public String customerName() {
return dummy;
}
}
14. Row Fixture (2)
import fit.RowFixture;
public class AccountListingFixture extends RowFixture {
public Object[] query() throws Exception {
List result = new ArrayList();
return result.toArray();
}
public Class getTargetClass() {
return AccountListingItem.class;
}
}
15. Domänenspezifische Fixtures
• wenn keine Fixture-Grundart so richtig passt
• Erweiterungen von fit.Fixture über Hook-
Methoden:
• doTables(Parse tables)
• doTable(Parse table)
• doRows(Parse rows)
• doRow(Parse row)
• doCells(Parse cells)
• doCell(Parse cell, int index)
16. Übung 1:
Akzeptanztestgetriebene Entwicklung
• Bringen Sie die Testfälle in
acceptance-tests/RichCalc.html
schrittweise zum laufen!
• (TaschenrechnerFixture.java
ist eine ColumnFixture)
17. Nachlese Übung 1
• Probleme bei akzeptanztestgetriebener
Entwicklung
• (Zu) große Schritte: Es dauert oft zu lange,
einen Test auf grün zu bringen
• Keine Möglichkeit, Design zu erzwingen
• Keine Möglichkeit, Module isoliert ins Leben zu testen
• Kombinatorische Explosion
• Lösung: Unit Tests
• Viele kleine Unit Tests schreiben
• Jeden Akzeptanztest schrittweise erfüllen
18. JUnit - Testframework für Java
• Java-Framework zum Schreiben und Ausführen
automatischer Unit Tests
• Tests werden in Java codiert
• Ist auch für zahlreiche andere
Programmiersprachen erhältlich
• http://junit.org
19. Beispiel: Test
import org.junit.Test;
import static org.junit.Assert.*;
public class EuroTest {
@Test
public void cents() {
Euro two = new Euro(2.00);
assertEquals(200, two.getCents());
}
}
20. Anatomie eines Testfallklasse
• Testfallmethoden @Test public void ...()
• Verwendung der statisch importierten Methoden
assert...() und fail()
• Fehlschlag von assert...() beendet den Testfall
• Instanzvariablen für Testobjekte
• @Before public void ...()
zum Aufbau von Testobjekten und Testressourcen
• @After public void ...()
zur Ressourcenfreigabe
21. Beispiel: Test Suite
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(value = Suite.class)
@SuiteClasses( { CalculatorTest.class })
public class AllTests {
}
24. Test/Code/Refactor – Schritte
• grün-rot: Schreibe einen Test, der zunächst
fehlschlagen sollte. Schreibe gerade soviel Code,
dass der Test fehlschlägt.
• rot-grün: Schreibe gerade soviel Code, dass alle
Tests laufen.
• grün-grün: Eliminiere Duplikation und andere
üble Codegerüche.
25. Programmierzug grün - rot
• Programming by Intention:
Der Test verwendet die zu testende Funktion,
als wäre sie schon realisiert.
• Die Klassenschnittstelle wird im Test
aus der Perspektive eines Verwenders entworfen.
• Jeder Test geht einen kleinen Schritt.
• Den Test zunächst fehlschlagen zu sehen,
testet den Test selbst.
26. Programmierzug rot - grün
• Nur soviel Code schreiben, wie die Tests
erfordern.
• Mehr wäre weder durch die Tests spezifiziert,
noch abgesichert.
• Erst weitere Tests beweisen, dass die Lösung
eventuell noch zu einfach ist.
27. Refactoringzug grün - grün
• Eliminierung von Duplikation!
• Verbesserungen am Design des Programms, ohne
sein beobachtbares Verhalten zu ändern,
• um die Verständlichkeit zu erhöhen
• um Designschwächen zu beheben
• um die Änderbarkeit zu erhalten.
28. Strikte Unit Tests
• Ein Unit Test soll eine Klasse
oder ein Klassenteam in Isolation testen.
• Probleme:
• Programmeinheiten arbeiten nicht isoliert.
• Aufbau der Testumgebung ist oft aufwändig.
• Testen von Ausnahmesituationen
• langsame Tests bei Überschreiten der Systemgrenzen
29. Isoliertes Testen
• Für die Dauer der Tests ersetzen wir
Abhängigkeiten zu mitwirkenden
Programmeinheiten durch die Einführung
einfacher Attrappen.
• Vorteile:
• Tests laufen schnell.
• Auftretende Fehler sind leicht zu lokalisieren.
• Notwendige Testkombinatorik ist kleiner
als beim Testen mehrerer Objekte.
31. Mock-Objekte
• Ersetzen von der zu testenden Unit verwendeten
Klassen oder Schnittstellen während des Tests
• Vorgehen:
• Verhalten und Erwartungen spezifizieren
• Unit unter Verwendung des Mockobjekts testen
• korrekte Verwendung verifizieren
32. EasyMock – Dynamische Mock-Objekte
• Erzeugung eines Mock-Objekts „on the fly“:
• MyInterface mock =
createMock(MyInterface.class);
• Aufzeichnen des erwarteten Verhaltens:
• expect(mock.myMethod(42))andReturn(Y);
replay(mock);
• Verifikation der Verwendung des Verhaltens:
• verify(mock);
• http://easymock.org/
33. Systemgrenzen im Test
• Dummies und Mocks:
• Was wir zur Isolation innerhalb des Systems verwenden,
funktioniert auch an Systemgrenzen.
• Bei komplexen Systemgrenzen:
• Fassade/Adapter zur zusätzlichen Indirektion einführen
• Testen vom System zu Fassade/Adapter
• Testen von Fassade/Adapter zur Systemgrenze
• funktionale Tests testen das Zusammenspiel
34. Übung 2: Isolierte Unit Tests
• Bringen Sie die Testfälle in
acceptance-tests/RichCalc.html
schrittweise zum laufen!
• Diesmal aber:
• Schreiben Sie Unit Tests für die Akzeptanztests
• Setzen Sie Mockobjekte ein, falls nötig
• Startlinie: calculator.unittests.CalculatorTest
35. Code Coverage
• Wie gut sind unsere Tests?
• Wie groß ist die Abdeckung des Codes durch
unsere Tests?
• http://www.cenqua.com/clover/
• http://works.dgic.co.jp/djunit/
• http://www.eclemma.org/
• Welche Code-Änderungen werden durch unsere
Tests entdeckt?
• http://jester.sourceforge.net/
• Metriken als Spiegel der Gewohnheit
36. Testen von Legacy Code
• Legacy Code ist Code ohne automatisierte Tests!
• Michael Feathers:
„Code without tests is bad code. It doesn't matter
how well written it is [...] With tests, we can
change the behavior of our code quickly and
verifiably. Without them, we really don't know if
our code is getting better or worse.“
37. Legacy Code Dilemma
• Um Software sicher zu ändern,
benötigen wir automatisierte Tests.
• Um automatisierte Tests zu schreiben,
müssen wir die Software ändern.
• Refactoring ohne Tests unumgänglich
38. Testen von bestehendem Code
• Änderung des bestehenden Systems
• neue Features
• Entfernen von Bugs
• Designverbesserungen
• Optimierungen
• Vergrößerung der Testabdeckung
• Testgetriebene Entwicklung
• Charakterisierende Tests
• Integrationstests
39. Störende Abhängigkeiten
• Eine Klasse lässt sich nicht instanzieren
• Eine Methode lässt sich nicht ausführen
• Das Testergebnis lässt sich nicht ermitteln
42. Einschleusen von Mocks
• als Parameter
• über testbare Unterklasse
• mittels Überlagerung im Klassenpfad
• mit aspektorientierter Programmierung
• in C/C++ auch über Linker und Präprozessor
• Jedes Testproblem lässt sich über eine weitere
Indirektion lösen
43. Meine Klasse lehnt sich
gegen den Test auf
• Ursachen:
• Klasse kann nicht instanziert werden
• Hinderliche Konstruktor-Parameter
• Konstruktor hat Seiteneekte
• Techniken:
• Extrahiere und überschreibe Methode
• Extrahiere Interface
• Parametrisiere Konstruktor
44. Extrahiere und Überschreibe (1)
public class A {
public class A {
public A() {
public A() {
B b = makeB();
B b = new B();
b.doStuff();
b.doStuff();
...
...
}
}
protected B makeB() {
}
return new B();
}
}
45. Extrahiere und Überschreibe (2)
public class TestableA extends A {
protected B makeB() {
return new DummyB();
}
}
public class DummyB extends B {
public void doStuff() {
}
}
46. Extrahiere Interface
public class A {
public class A {
public A() {
public A() {
IB b = makeB();
B b = makeB();
b.doStuff();
b.doStuff();
...
...
}
}
protected IB makeB() {
protected B makeB() {
return new B();
return new B();
}
}
}
}
47. Parametrisiere Konstruktor
public class A {
public class A {
private IB b;
public A() {
public A(IB b) {
IB b = makeB();
this.b = b;
b.doStuff();
b.doStuff();
...
...
}
}
}
public A() {
this(new B());
}
}
48. Weitere Techniken
• Abzweigende Methode
• Abzweigende Klasse
• Statisch gemachte Methoden
• Primitivere Parametertypen
• Führe statischen Setter ein
• Führe delegierende Instanz ein
• Ersetze Singleton durch direkte Abhängigkeit
• Extrahiere Methoden
• Extrahiere Methodenobjekt
49. Verbesserungen über die Zeit
• Viele der Techniken machen das Design zunächst
einmal hässlicher
• Durch die ersten installierten Tests sind kleine
Verbesserungen möglich
• Kleine Verbesserungen lassen weitere
Refactoringkandidaten erkennen
• Schrittweise zu evolutionärem Design
50. Testen von GUIs und Web-Applikationen
• Wichtigster Grundsatz: Keine Logik im View
• Entwurfsmuster zur Trennung von Präsentation
und Geschäftslogik:
• Application Facade
• Model View Presenter
51. Application Facade
interface
BookSearchFacade
BookSearchDialog
searchTitles(searchExpression:String) : List
Book
Title
Author
52. Application Facade mit Observer
interface
Observer
changeOccurred()
implements
interface
BookSearchFacade
BookSearchDialog
searchTitles(searchExpression:String) : List
registerObserver(observer: Observer)
changeOccurred()
Book
Title
Author
54. Testen mit Skriptsprachen
• Dynamisch typisierte Skriptsprachen können das
Testen vereinfachen
• Beispiel Groovy:
• Alle Java-Features und Bibliotheken verfügbar
• Typisierung ist optional
• „Duck-Typing“ ist möglich
• Einfache Syntax für Blocks und Iteratoren
57. Groovy: Dynamisch typisierte Objekte
• „Anonyme“ Objekte on the fly
def display = {assert it == displayText} as IDisplay
def display = [display: {println it},
enableDecimalSeparatorKey: { ... },
disableDecimalSeparatorKey: { ... }
] as IDisplay
58. Übung 3: Wie es euch gefällt
• Variante 1: Schreiben Sie mit Hilfe von Jemmy eine
Taschenrechner-Fixture, die direkt die Swing-GUI
bedient
• Variante 2: Erweitern Sie die Groovy-Testsuite im
Package groovy.calculator.unittests
• Variante 3: Erweitern Sie GUI und GUI-Tests:
public interface IDisplay {
void display(String toDisplay);
void disableDecimalSeparatorKey();
void enableDecimalSeparatorKey();
}
60. Referenzen
• Michael Feathers: Working Eectively with Legacy
Code. Prentice Hall, 2004.
• Johannes Link: Softwaretests mit JUnit -
Techniken der testgetriebenen Entwicklung.
Dpunkt-Verlag, 2005.
• Rick Mugridge, Ward Cunningham: Fit for
Developing Software. Prentice Hall, 2005.
• Frank Westphal: Testgetriebene Entwicklung mit
JUnit FIT. Dpunkt-Verlag, 2005.