Klar formulierte Tests

In der Vergangenheit habe ich häufig Tests gesehen (und auch selber geschrieben), die schwer zu warten waren, da sie nicht klar formuliert waren. Nehmen wir beispielsweise folgenden (zugegebenermaßen noch recht einfachen) Test als Beispiel:

final Booking DOOR2DOOR_BOOKING = aBooking().withDefaultValues()
            .withOriginLocation(aLocation().withDefaultDoorValues().build())
            .withDestinationLocation(aLocation().withDefaultDoorValues().build())
            .withIncoterm(IncotermType.CIP)
            .withPackageDetails(aPackageDetails().withDefaultValues().build())
            // ... hier werden noch viele weitere Felder gesetzt
            .build();

@Test
public void shouldConvertTotalWeight() {
    // when
    final PackageDetailsComponent actual = converter.createPackageDetails(DOOR2DOOR_BOOKING);
 
    // then
    assertThat(actual.getTotalWeight()).isEqualTo(DOOR2DOOR_BOOKING.getTotalWeight());
}

// viele weitere Tests die auf DOOR2DOOR_BOOKING zugreifen

Der obige Test drückt sich nicht klar aus.  Dadurch könnten selbst bei einem so einfachen Test folgende Fragen bei dem Leser des Tests aufkommen:

  • Auf Grund des Namens DOOR2DOOR_BOOKING könnte sich der Leser fragen, warum ein Door to Door Booking benötigt wird, damit das Feld totalWeight konvertiert werden kann.
  • Wenn sich der Leser die Initialisierung von DOOR2DOOR_BOOKING ansieht, könnte er ziemlich verwirrt werden, da dieses Objekt auch noch von anderen Tests verwendet wird, die andere Felder von DOOR2DOOR_BOOKING  wie bspw. das Feld incoterm benötigen. Er muss daher recherchieren, welche dieser Felder für den Test wichtig sind. Es könnte bspw. sein, dass das Feld totalWeight nur in bestimmten Fällen kopiert wird. Das ist schwer offensichtlich wenn die wichtigen Felder, die für den Test gesetzt sein müssen, in der Masse der für den Test unwichtigen Felder (wie bspw. incoterm), untergeht.
  • Da das Objekt, das auf DOOR2DOOR_BOOKING zugewiesen wurde, von mehreren Tests verwendet wird, müsste man prüfen, ob es immutable ist. Wenn nein, so könnte es von anderen Tests verändert werden, was zu instabilen Tests führen kann und zusätzlich verwirrt.
  • Auch würde er sich vielleicht fragen, ob der Test überhaupt etwas testet, da der Test auch durchlaufen würde, wenn das Feld totalWeight sowohl in actual als auch in DOOR2DOOR_BOOKING null wären.

Für viele dieser Fragen würde man Vermutungen anstellen. Ich würde beispielsweise vermuten, dass das Feld einfach (unabhängig von irgendwelchen anderen Feldern) kopiert wird. Auch würde ich vermuten, dass das Feld totalWeight mit einem Wert initialisiert ist und somit auch durch die Zeile assertThat(actual.getTotalWeight()).isEqualTo(DOOR2DOOR_BOOKING.getTotalWeight()) auch wirklich geprüft wird, dass ein Wert kopiert wird. Eventuell wird der Leser sogar in den Code abtauchen, um seine Vermutungen zu verifizieren. Das ist schlecht, denn eigentlich möchte man nicht, dass sich der Leser unnötige Fragen stellt. Stattdessen sollte jeder Test für den Leser offensichtlich sein. Er sollte die Tests einer Klasse überfliegen und sehr schnell darüber erfassen können, wie eine Klasse funktioniert.

Schreibt man den Test nun ein wenig um, so kommuniziert der Test deutlich klarer, was wichtig ist:

@Test
public void shouldConvertTotalWeight() {
    // given
    final PreciseAmount<Mass> weight = PreciseAmount.valueOf(120, SI.KILOGRAM);
    final Booking booking = someBooking().withTotalWeight(weight).build();

    // when
    final PackageDetailsComponent actual = converter.createPackageDetails(booking);

    // then
    assertThat(actual.getTotalWeight()).isEqualTo(weight);
}

Der Leser sieht hier sofort, dass wir nur irgendein Booking benötigen, bei dem das Feld totalWeight gefüllt ist. Dieses kommuniziert das Builder Pattern someBooking().withTotalWeight(weight).build(). Die Methode someBooking() baut irgendein Booking zusammen, so dass der Aufruf der Methode createPackageDetails(booking) nicht zu einer Exception führt. Danach werden dann die Felder überschrieben, die für unseren Test wichtig sind. Im obigen Fall wird lediglich das Feld totalWeight im Booking gesetzt. Damit wird an den Leser kommuniziert, dass nur dieses eine Feld für den Test wichtig ist. Er sieht also auf den ersten Blick, dass dieses eine Feld in diesem Test lediglich kopiert wird. Auch ist nun klar, dass das booking.totalWeight ungleich null ist und somit auch wirklich kopiert wird. Weiter erzeugt sich nun jeder Test ein neues Booking-Objekt so dass kein anderer Test es verschmutzen kann selbst wenn es mutable ist.

Das war nun noch eine ziemlich einfache Methode, die nur ein Feld kopiert. Wird die Methode die getestet wird und der zugehörige Tests komplizierter, ist es noch wichtiger drauf zu achten, dass der Test verständlich ausgedrückt ist. Ansonsten muss man später bei Anpassungen am Produktionscode zusätzliche Zeit aufwenden, um überhaupt erst einmal die Tests zu verstehen und zu refaktorisieren.

Jira Command-Line Client

Neuerdings verwenden wir in meinem Team ein physikalisches Kanban Board. Trotzdem haben wir noch ein Jira für folgende Dinge:

  • Zu Dokumentationszwecken. Dafür beinhaltet die Commit Message die Jira Ticketnummer. Stolpert ein Entwickler über eine Codestelle, kann er über die Commit ID das zugehörige Jira Ticket öffnen und sich somit Hintergrundinformationen besorgen.
  • Pflegen von Zeitaufwänden zur Abrechnung.
  • Unsere Continous Deployment Pipeline verwendet bspw. die Jira Tickets. So setzt diese nach dem Produktionsdeployment einen Status so dass sich unser Support über einen Jira Filter immer ansehen kann, welche Tickets zuletzt auf Produktion ausgerollt wurden. Auch führt diese ein Produktionsdeployment nur durch, wenn alle Tickets, die neu auf Produktion ausgerollt werden würden, abgenommen wurden und sich somit im richtigem Status befinden.

Auch mit physikalischem Board brauchen wir das Jira also noch. Glücklicherweise nicht mehr so häufig. Trotzdem muss man manchmal ein Ticket kommentieren oder den Status eines Tickets ändern. Das ist zumindest in meiner Firma recht umständlich da ich häufig ausgeloggt werde und der Webclient sehr langsam ist sowie man viel klicken muss. Nun bin ich durch einen Kollegen auf den Jira Command-Line Client von Netflix aufmerksam gemacht worden, mit dem sich Jira mit ein bisschen Bash Magic sehr bequem aus der Kommandozeile heraus bedienen lässt – unter Windows benutze ich übrigens Cmder inkl. Git als Kommandozeile.

Beispielweise kann man sich ein Jira Ticket über jira view <Ticketnummer> direkt in der Kommandozeile ansehen. Über jira take <Ticketnummer> kann man das Ticket sich selber zuweisen. Das funktioniert gut. Nun ist es allerdings so, dass wir bei uns in der Abteilung die Namenskonvention haben, dass die Jira Ticketnummer Bestandteil des aktuellen git branches ist (topic/<Ticketnummer>). Der zum Ticket KNFN-8918 gehörige Branch hat bspw. den Namen topic/KNFN-8918. Ist man also gerade in dem Projektordner, kann man sich über den folgenden git Befehl die Jira Ticketnummer aus dem Branchnamen parsen:

git rev-parse --abbrev-ref HEAD | sed -e "s|.*/||"

Nun habe ich mir folgende Aliase in meiner .bashrc angelegt:

alias current_jira_issue='git rev-parse --abbrev-ref HEAD | sed -e "s|.*/||"'

alias jira_view_issue='jira view $(current_jira_issue)'

alias jira_take_issue='jira take $(current_jira_issue)'

alias jira_comment_issue='jira comment $(current_jira_issue) -m '

Dadurch muss ich für die Befehle, die ich häufig benutze, die Jira Ticketnummer nicht mehr eingeben sondern sie wird automatisch aus dem aktuellen Git Branchnamen geparst. Gibt man bspw. jira_view_issue ein, so wird das Ticket zum aktuellen Branch in der Kommandozeile angezeigt:

Insgesamt bin ich recht begeistert von dem Jira Command-Line Client von Netflix. Er ist extrem schnell und ich muss nicht mehr so häufig den langsamen Web-Client bedienen. Außerdem ist dank Cmder (im Quake Modus) unter Windows die Kommandozeile nur eine Tastenkombination entfernt, so dass man schnell nur mit Tastenkombinationen zwischen IDE und Kommandozeile wechseln kann.

Ein paar kleine Kinderkrankheiten hat es zwar noch. So hat der Client beispielsweise aktuell Probleme mit unserem Firmenproxy. Dieses umgehe ich aber so, dass ich in dem Tab der Bash, in dem ich mit dem Jira interagiere, den Proxy per Bash Funktion ausstelle. Auch muss man aktuell (Version 4.0) manuell noch ein .jira.d Verzeichnis im Home-Verzeichnis anlegen da es ansonsten zu einem Fehler kommt da der Client keine cookies.js Datei anlegen kann. Rein grundsätzlich kann ich das Projekt nur empfehlen.

Troubleshooting von Spring MVC

Letztens hatten wir auf der Arbeit einen Fall wo nicht klar war, warum ein Request nicht beim Spring MVC Controller ankam. Die Ursache kann bei solchen Problemen normalerweise recht schnell gefunden werden, in dem man das DispatcherServlet von Spring debugged. Dazu am besten einen Breakpoint in dessen doDispatch Methode setzen und den Request danach erneut ausführen. Die Methode ist verständlich und man erkennt beim schrittweisen Ausführen der Methode normalerweise recht schnell, warum ein Request nicht wie erwartet verarbeitet wird. Häufig findet man bei genauere Betrachtung der getHandler Methode des Spring DispatcherServlets, die von der doDispatch Methode aufgerufen wird,  den Übeltäter:

Spring DispatcherServlet getHandler method

In dieser wird eine Liste von Handlermappings durchlaufen um zu prüfen, ob der aktuelle Request auf eines davon passt. Wird eine mit @RequestMapping annotierte Methode nicht aufgerufen, lohnt es sich bspw. bis zum Aufruf des RequestMappingHandlerMappings vorzulaufen und dann  zu debuggen, was diese für Mappings beinhaltet und warum keines davon auf den aktuellen Request passt. Ist man hier immer noch ratlos, vergleicht man es am besten noch mit einem Request, der erfolgreich aufgelöst werden kann.

Durch diese Vorgehensweise kann man häufig recht schnell die Lösung finden wenn man alles offensichtliche, wie bspw. falsche Pfade oder falsche Http Methode, schon geprüft hat.