Test-Driven Development: Unit Tests mit Scala entwickeln - JAXenter
Mit guten Vorsätzen ins neue Jahr

Test-Driven Development: Unit Tests mit Scala entwickeln

Andreas Kaltenbach

Lerne jedes Jahr eine neue Programmiersprache – dies haben die Pragmatic Programmers Andy Hunt und David Thomas schon 1999 jedem Software Engineer ans Herz gelegt. Und warum könnte die neue Sprache dieses Jahr denn nicht Scala sein? Doch wie schaffe ich es, die neue Sprache in mein alltägliches Projektumfeld einzubringen? Tests in Scala zu schreiben, klingt nach einem ersten und zudem sanften Schritt in eine schöne neue Scala-Welt.

Mit Scala ist in diesem Fall nicht das berühmte Mailänder Opernhaus gemeint, sondern vielmehr die Programmiersprache Scala, entwickelt an der EPFL in Lausanne. Scala ist wie Java eine statisch typisierte Sprache, die zudem Konzepte der objektorientierten und funktionalen Programmierung in einer Sprache vereint. Scala wurde initial für die JVM als Zielplattform entwickelt, ist aber mittlerweile auch auf der .NET-Plattform zuhause. Scala-Anhänger schätzen die Sprache, da sie vor allem durch sehr ausdrucksstarken und kompakten Code brilliert und sich zu 100 % auf objektorientierte Paradigmen besinnt. Im Vergleich zu Java erscheint die Sprache somit mächtiger und eröffnet Entwicklern völlig neue, oft ungeahnte Möglichkeiten. Dank Scala gelingt es Entwicklern, schneller und direkter zu ihrem Ziel zu gelangen. Wie können also diese positiven Eigenschaften der Sprache in unseren täglichen Projekten genutzt werden? An welchen Stellen kann Scala uns sinnvoll unter die Arme greifen?

Das leidige Thema Tests

Das Testen von Software rangiert vermutlich auf Platz 2 der unbeliebtesten Entwicklertätigkeiten, gleich hinter Softwaredokumentation. Durch testgetriebene Entwicklungsansätze hat sich jedoch innerhalb der letzten Jahre unser Verständnis von Softwaretests sehr stark verändert. Die Ermittlung von Softwarequalität durch automatisierte Softwaretests ist weit verbreitet und gehört heute mehr oder weniger zum Handwerkszeug eines jeden professionellen Softwareentwicklers. Das Erstellen und Warten von Tests nimmt also einen höheren Stellenwert ein und führt dazu, dass Entwickler sich weitaus mehr mit Tests beschäftigen müssen, als ihnen unter Umständen lieb ist. JUnit oder TestNG beschreiben standardisierte Wege, um Softwarekomponenten automatisiert zu testen. Sie entsprechen aber lediglich der Spitze des Eisbergs heutiger Test Suites.

Abb. 1: Exemplarischer Aufbau von Testkomponenten

Abbildung 1 zeigt, dass zwischen den Test Suites und den Produkten von Drittanbietern in der Regel noch weitere unterstützende Bibliotheken entstehen, die testübergreifende Funktionalität kapseln. Beispiele dafür sind Test Fixtures [1] bzw. Object Mothers [2], die eine einheitliche und testübergreifende Schnittstelle zur Erzeugung von Testobjekten bieten. Für Integrationstests existieren oftmals weitere Utilities, die die Peripherie wie LDAP- und Mail-Server oder auch proprietäre Drittsysteme simulieren. Es ist nun an der Zeit, Scalas Stärken auszuspielen und zu sehen, wie mithilfe von Scala die Tests inklusive der Testinfrastruktur noch schneller und vor allem eleganter umgesetzt werden können. Im Idealfall gewinnen wir dadurch zusätzliche Zeit für die schönen Dinge im Leben eines Entwicklers.

Scala ungeschminkt

Neben TDD-Patterns haben sich im Laufe der Jahre leider auch einige Anti-Patterns etabliert. Eine schöne Sammlung an Anti-Pattern-Klassikern hat James Carr in seinem Blog [3] zusammengestellt. In einigen Fällen sind es allerdings die Unzulänglichkeiten der eingesetzten Programmiersprache, die Entwickler in solche Fallen laufen lassen. Mit einfachen Scala-Bordmitteln können wir in vielen Fällen solche Fallen umgehen.

Abb. 2: Vererbungshierarchien in Java

Nehmen wir z. B. die in Abbildung 2 dargestellte Vererbungshierarchie. Eine abstrakte Testklasse ist für die Erzeugung von gemeinsamen Testdaten verantwortlich, eine weitere Spezialisierung davon startet für Testzwecke einen Server, der die Schnittstelle eines Drittsystems simuliert. Für den ersten Augenblick erscheint das Setup sinnvoll, das Don’t-Repeat-Yourself-(DRY-)Prinzip wird eingehalten und Codeduplikation vermieden. Durch die Vererbung werden für Test 2 und Test 3 immer Testobjekte erzeugt. Nehmen wir aber einmal an, dass Test 3 lediglich Code zur Kommunikation mit der Serverschnittstelle testet und gänzlich auf Testdaten verzichten kann. Durch Javas Einfachvererbung kann dieser Konflikt nicht hinreichend zufriedenstellend gelöst werden. Orthogonale Belange werden zwangsweise der einfachen Vererbungshierarchie in Java untergeordnet. Das Beispiel ist zwar stark vereinfacht, allerdings sind bis hin zu einem halben Dutzend aufeinander aufbauende, abstrakte Testklassen absolut keine Seltenheit. Scala kennt für solche Zwecke so genannte Traits, die in diesem Fall wie die Faust aufs Auge passen. Eine Scala-Klasse kann beliebig viele Traits erweitern. Jedes Trait implementiert sein Verhalten in Methoden. Listing 1 zeigt, wie für das Beispiel die Vermischung der Belange über Traits aufgehoben werden kann.

Listing 1:
trait TestDataCreating {
  def createTestData : {...}
}
	
trait MockServerStarter {
  def startMockServer : {...}
}
	
class Test1 extends TestDataCreating 
class Test2 extends TestDataCreating with MockServerStarter
class Test3 extends MockServerStarter

Doch schauen wir uns doch noch einmal die Aufgaben der einzelnen Hilfsklassen genauer an: Wir haben u. a. eine gemeinsame Klasse für die Bereitstellung von Testdaten. Sie versorgt alle Unit Tests mit denselben Testdaten. Zunächst erscheint das Vorgehen sinnvoll, da das Erzeugen der Testdaten in einer einzelnen Komponente gekapselt ist. Doch eine gemeinsame Testdatenbasis hat auch ihre Nachteile. Tests sind nicht voneinander isoliert und nicht in sich selbst abgeschlossen, da sie sich gemeinsame Testdaten teilen. Jeder Test muss sicherstellen, dass die Testdaten im Ursprungszustand an den nächsten übergeben werden. Modifiziert ein zuvor ausgeführter Test die gemeinsamen Testdaten, so schlagen Tests fehl, obwohl Test und auch die zu testende Komponente in fehlerfreiem Zustand sind. Und schon sind wir auf James Carrs Anti-Pattern des Peeping Tom bzw. The Uninvited Guest gestoßen: Die Tests sind aufgrund gemeinsamer Ressourcen aneinander gekoppelt und nicht fehlerfrei aufräumende Tests können das Fehlschlagen von nachfolgenden Tests verursachen.

Geschrieben von
Andreas Kaltenbach
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.