Suche

Test-Driven Development: Unit Tests mit Scala entwickeln

Scala Test

Bis zu diesem Punkt haben wir gesehen, wie ausschließlich Scalas Bordmittel eingesetzt werden können, um das Schreiben von Testcode zu erleichtern, losgelöst von jeglichem Testframework. Aber natürlich darf für eine viel versprechende Sprache wie Scala ein Testframework nicht fehlen. Scala Test [7] füllt diese Lücke, indem es gängige Frameworks wie JUnit oder TestNG aufgreift und Scala-Konzepte einfließen lässt. Das Resultat ist ein Testframework, mit dem Testcode deutlich ausdrucksstärker und verständlicher geschrieben werden kann. Mit Scala Test kann die Geschichte eines Tests mit einfacheren Worten erzählt werden, sodass das Publikum noch einfacher der Handlung folgen kann. Nehmen wir z. B. unsere Protagonisten Madonna, Michael Jackson und Jesus. Um sicherzustellen, dass der Wiederbelebungsservice wirklich alle Berühmtheiten wiederbeleben kann, reicht ein einfacher assert-Ausdruck aus: assert(celebrities.forall(_.isAlive)).

Im Gegensatz zu JUnit oder TestNG kommt Scala Test aufgrund Scalas funktionalem Charakter mit einem einzelnen assert aus. Das Scala Test API abstrahiert zudem von den zugrunde liegenden Testframework-APIs. Egal ob mit JUnit oder TestNG ausgeführt, der Testcode bleibt identisch. Die Möglichkeit, Scala um eigene Sprachkonstrukte zu erweitern, demonstriert Scala Test beim Überprüfen von Exceptions. Um sicherzustellen, dass unser Wiederbelebungsservice eine PersonAlreadyAliveException für bereits lebendige Personen wirft, reicht der folgende Ausdruck aus:

intercept[PersonAlreadyAliveException] {
      celebrityService.resurrect(madonna)
    }

Wird beim Aufruf des Service keine PersonAlreadyAliveException geworfen, so schlägt der Test fehl. intercept ist eine typisierte Scala-Methode, die geschweiften Klammern können problemlos durch runde ersetzt werden, ohne die Funktionalität zu verändern. intercept erspart somit umständliches Hantieren mit try/catch-Blöcken oder die Angabe von erwarteten Exceptions in @Test-Annotationen.

Wer seine Tests gerne etwas näher an der natürlichen Sprache sehen möchte, kann dies in Scala über die so genannte Matcher DSL erreichen. Wer z. B. zu überprüfen will, dass Michael Jackson nach der Wiederbelebung wirklich lebt, kann dies mit Scala Test so ausdrücken: (michaelJackson.isAlive) should be (true).

Auch Mocks und deren Verhalten können über Scala Test konsequenter und verständlicher spezifiziert werden. EasyMock [8] ist ein hervorragendes Mock Framework, aber das Aufsetzen der Mocks ist in Java mit viel, zum Teil schwer verständlichem Code verbunden. In EasyMock durchläuft jeder Mock die drei Phasen expect, replay und verify. In Phase 1 wird das Verhalten des Mocks bestimmt, die Phasen 2 und 3 dienen dazu, den Mock abzuspielen und ihn am Ende des Tests zu validieren. Jede Phase wird über API-Aufrufe gesteuert, was bei mehreren Mocks sehr schnell unübersichtlich und fehleranfällig werden kann. In Scala Test ist der Einsatz von Mocks mit deutlich weniger Schmerzen verbunden. Ein letztes Mal nehmen wir den Wiederbelebungsservice, den wir nun für Testzwecke mocken wollen. Listing 4 enthält eine Testmethode, die einen Mock des Wiederbelebungsservice erzeugt und verwendet.

Listing 4:
@Test def testUsingMocks = {
  val celebrityServiceMock = mock[CelebrityService]
			
  // 1. define expectation/behavior
  expecting {
    celebrityServiceMock.resurrect(michaelJackson) andReturn true
  }
			
			
  // 2. EasyMock replay and verify
  whenExecuting(celebrityServiceMock) {
				
    // perform tests
// ...
  }
}

Scala Test unterscheidet nur noch zwischen zwei Mock-Phasen: dem Spezifizieren der Erwartungshaltung und dem Abspielen mit gleichzeitigem Validieren des Mocks. Für jede der beiden Phasen existiert eine eigene Kontrollstruktur. Der eigentliche Testcode läuft innerhalb des whenExecuting-Blocks, die Mocks werden als Argumente in den Block übergeben. Wird der Block verlassen, so werden die Mocks automatisch validiert. Somit ist immer sichergestellt, dass jeder abgespielte Mock auch tatsächlich am Ende überprüft wird.

Scala Test hat noch einige weitere Pfeile im Köcher, vor allem im Bereich Behaviour Driven Development (BDD). Wer mehr zu Scala Test erfahren möchte, dem sei Bill Venners durchaus sehenswerter Vortrag zum Thema empfohlen [9].

Scala Check

Die bisher vorgestellte Domäne war zugegebenermaßen einfach gehalten. In der Realität werden wir aber alle mit weitaus komplexeren Objektstrukturen konfrontiert. Um solch komplexe Objektgrafen schnell und einfach für Testzwecke zu erzeugen, eignet sich Scala Check [10] besonders gut. Scala Check ist eine Implementierung der ursprünglich für Haskell geschriebenen QuickCheck-Bibliothek. QuickCheck wurde an der Technischen Universität Chalmers in Göteborg entwickelt und ist ein Werkzeug, um Software automatisiert durch generierte Testfälle zu validieren. Annahmen bzgl. des erwarteten Systemverhaltens werden im Test beschrieben und anschließend durch generierte Testdaten verifiziert. QuickCheck bzw. Scala Check stellen Generatoren für primitive Datentypen bereit: val stringGenerator = Gen.alphaStr.

Der obige Code legt einen Scala Check Generator zur Erzeugung zufälliger, alphanumerischer Strings an. Basierend auf den primitiven Generatoren lassen sich eigene Generatoren zusammenbauen, die in der Lage sind, komplexe prototypische Geschäftsobjekte zu erzeugen. Listing 5 zeigt einen Generator zur Erzeugung fachlich gültiger Berühmtheiten.

Listing 5:
val personGenerator = for {
  name 

Eine frische Berühmtheit lässt sich einfach durch folgende Zeile erzeugen: val celebrity = personGenerator.sample.

Scala Check Generatoren lassen sich beliebig kombinieren, um noch ausgereiftere Generatoren zu erzeugen: val boybandGenerator = Gen.containerOfN[List, Person](4, personGenerator).

Die obige Anweisung erzeugt einen Generator, der immer eine neue Liste mit genau vier Berühmtheiten erzeugt. Scala Check Generatoren erzeugen also stets frische Prototypen on-demand. Der Lebenszyklus der erzeugten Testobjekte erstreckt sich idealerweise bis an das Ende eines Tests, womit die Testobjekte als Einwegartikel angesehen werden können. Die QuickCheck-Implementierung für Java [11] bringt auch Java-Entwickler in den Genuss von generierten Testobjekten.

Ab Version 2.8 können in Scala sog. Package Objects genutzt werden. Ein Package Object implementiert Package-interne Methoden oder Felder, die von anderen Komponenten innerhalb des Packages wiederverwendet werden können. Es bietet sich an, Scala Check Generatoren als implizite Package Objects abzulegen, da diese impliziten Generatoren somit von jedem einzelnen Unit Test innerhalb des Packages wiederverwendet werden können.

Kommentare

Schreibe einen Kommentar

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