Blog

Wenden Sie TDD auf Hadoop-Jobs mit Scoobi an

Maarten Winkels

Aktualisiert Oktober 22, 2025
5 Minuten
Map Reduce ist ein Programmiermodell zum Schreiben von Algorithmen, die große Datenmengen in (relativ) kurzer Zeit verarbeiten. Die Bausteine für die Programme sind sehr einfache Map- und Reduce-Funktionen. Das Schreiben von Programmen, die auf der Grundlage dieser einfachen Funktionen immer komplexere Aufgaben mit Daten durchführen, wird immer schwieriger und erfordert daher in frühen Phasen gründlichere Tests. In diesem Blog wird versucht, eine einfache Methode zum Testen des Algorithmus eines Map-Reduce-Programms auf der Grundlage von scoobi zu skizzieren. Schauen wir uns das einfache Beispiel auf der scoobi-Website an: [scala highlight="11,12,13,14"] import com.nicta.scoobi.Scoobi._ import com.nicta.scoobi.DList._ import com.nicta.scoobi.io.text.TextInput._ import com.nicta.scoobi.io.text.TextOutput._ object WordCount { def main(allArgs: Array[String]) = withHadoopArgs(allArgs) { args => val lines: DList[String] = fromTextFile(args(0)) val counts: DList[(String, Int)] = lines.flatMap(_.split(" ")) .map(word => (word, 1)) .groupByKey .combine(_+_) persist(toTextFile(counts, args(1))) } } [/scala] Das Beispielprogramm zählt die Wörter in der Eingabedatei. Das Schöne am Scoobi-Framework ist, dass es Entwicklern erlaubt, mit Sammlungsabstraktionen zu arbeiten, um ihren Map-Reduce-Algorithmus auszudrücken, ohne sich mit den blutigen Details des direkten Schreibens von Mappern und Reduzierern befassen zu müssen. Die Details des Hadoop-Frameworks, das scoobi zugrunde liegt, sind schön hinter den einfach aussehenden Anweisungen in den Zeilen 9 und 16 versteckt. Ohne zu sehr ins Detail zu gehen, wird die Funktionsweise von scoobi in diesem Zitat auf der scoobi-Website zusammengefasst:
...führt der Aufruf von DList-Methoden nicht sofort dazu, dass Daten im HDFS erzeugt werden. Das liegt daran, dass Scoobi hinter den Kulissen einen Staging-Compiler implementiert. Der Zweck der DList-Methoden besteht darin, einen Graphen von Datentransformationen zu erstellen. Der Akt der Persistierung einer DList löst dann die Kompilierung des Graphen in einen oder mehrere MapReduce-Jobs und deren Ausführung aus.
Ich schlage nun vor, diesen scoobi-Ansatz noch einen Schritt weiter zu gehen und den Algorithmus nicht nur mit einer Sammlungsabstraktion zu entwickeln, sondern ihn auch mit einfachen Sammlungen zu testen! Vom Standpunkt des Testens aus betrachtet, gibt es zwei Annahmen, die ich durch das Testen des obigen Codes sicherstellen möchte:
  1. Die Ausführung des Codes sollte korrekte Hadoop-Mapper und -Reduzierer erzeugen und ausführen.
  2. Der Algorithmus sollte die Aufgabe erfüllen, für die er entwickelt wurde: das Zählen der Wörter in der Eingabedatei.
Der erste ist ein Integrationstest, der durchgeführt werden kann, indem die Aufträge auf Hadoop im Standalone-Modus ausgeführt werden. Dazu muss das Programm ausgeführt werden, um den Hadoop-Code zu erzeugen, und die erzeugten Aufträge müssen auf Hadoop ausgeführt werden. Der zweite Test kann durchgeführt werden, indem der Algorithmus (die vier Zeilen) gegen ein beliebiges sammlungsähnliches Objekt ausgeführt wird, das die erforderlichen Funktionen implementiert. Im Vergleich zum oben beschriebenen Test handelt es sich hierbei eher um einen Unit-Test. Es sollte viel einfacher sein, ihn zu schreiben und auszuführen und zu beweisen, dass ein kleiner, aber funktionell ziemlich wichtiger Teil des gesamten Programms korrekt abläuft. Lassen Sie uns klarstellen: Ein guter Unit-Test kann niemals einen Integrationstest ersetzen. Insbesondere in einer Situation wie dieser, in der der Unit-Test auf einer völlig anderen Plattform läuft, ist es sehr wichtig, sicherzustellen, dass das Endprodukt wie erwartet funktioniert. Das Testen des Algorithmus auf einfachen Sammlungen ermöglicht es Ihnen jedoch, komplexe Hadoop-Jobs im Geiste von TDD zu entwickeln und sicherzustellen, dass Teile des Systems korrekt funktionieren, bevor Sie es einsetzen und ausführen. Sehen wir uns das Beispiel an. Der Algorithmus zum Zählen von Wörtern ist in den vier hervorgehobenen Zeilen oben genau beschrieben. Der Algorithmus besteht im Wesentlichen aus vier einfachen Sammlungsmanipulationen, die im scoobi-Typ DList implementiert sind, aber die meisten davon sind bekannte Funktionen höherer Ordnung, die auch von Scalas Sammlungstypen implementiert werden. Um den Algorithmus zu testen, müssen wir ihn in eine separate Funktion extrahieren, die mit einer Abstraktion arbeitet, die dieselben Operationen implementiert. Diese Abstraktion kann für Unittesting auf einem der Scala-Sammlungstypen und für die Produktionssituation auf dem DList-Typ von Scoobi implementiert werden. [scala highlight="11,12,13,14,15"] object WordCount { def main(allArgs: Array[String]) = withHadoopArgs(allArgs) { args => val lines: DList[String] = fromTextFile(args(0)) val counts: DList[(String, Int)] = algorithm(lines) persist(toTextFile(counts, args(1))) } def algorithm(lines : ListWrapper[String]) : ListWrapper[(String, Int)] = lines.flatMap(_.split(" ")) .map(word => (word, 1)) .groupByKey .combine(_+_) } [/scala] Jetzt können wir Unittests wie diese schreiben: [scala] class WordCountSuite extends FunSuite { test("sollte einzelne Zeilen mit demselben Wort zählen") { val Ergebnis: List[(String, Int)] = WordCount.algorithme("Wort Wort Wort" :: Nil) assert(Ergebnis === ("Wort", 3) :: Nil) } test("sollte mehrere Male dieselbe Zeile zählen") { val Ergebnis: List[(String, Int)] = WordCount.algorithme("ein Wort ist ein Wort" :: "ein Wort ist ein Wort" :: Nil) assert(result.toMap === Map("a" -)> 4, "Wort" -> 4, "ist" -> 2)) } test("sollte einige Zeilen zählen") { val Ergebnis: List[(String, Int)] = WordCount.algorithme("dies ist eine Zeile" :: "und eine weitere Zeile" :: Nil) assert(result.toMap === Map("this" -)> 1, "ist" -> 1, "a" -> 1, "Linie" -> 2, "und" -> 1, "ein anderer" -> 1)) } } [/scala] Hinter den Kulissen wird im Test und im eigentlichen Programm eine unterschiedliche Implementierung für den Typ ListWrapper verwendet. Dies geschieht durch implizite Konvertierungen. Eine grobe Implementierung der erforderlichen Klassen finden Sie hier. Das Schreiben von Unit-Tests für diese Logik wird nun einfach. Es ist jedoch sehr wichtig, dass Sie verstehen, was getestet wird: Die Tests laufen gegen eine einfache In-Memory-Sammlung. Die Gewissheit, dass der Produktionscode auf einem Hadoop-Cluster das Gleiche tut, ergibt sich aus der Annahme, dass die Funktionen höherer Ordnung bei der Ausführung gegen die verteilte Sammlung die gleichen Ergebnisse liefern wie ihre In-Memory-Gegenstücke. Dies ist jedoch das Hauptziel des Scoobi-Frameworks: Entwicklern zu ermöglichen, ihre Hadoop-Aufgaben als einfache Sammeloperationen zu schreiben.

Verfasst von

Maarten Winkels

Contact

Let’s discuss how we can support your journey.