Unit Tests und was sie wirklich testen

Unit Tests und Agile Softwareentwicklung. Conway's Game of Life und Test Driven Development im Kontext Agiles Testing.
Eine einfache, aber mächtige Frage, die uns sofortiges Feedback über die Qualität eines Unit Tests gibt.

Unit Tests sind dazu da, Fehler früh zu erkennen, die Software besser zu designen (entkoppelt und gekapselt) und um eine robuste Regressions-Suite zu erhalten. Diese Tests sollen also unsere Softwareentwicklung beschleunigen, weil wir durch sie weniger Zeit mit dem Debuggen und Beheben von Fehlern verbringen müssen.

Dies funktioniert natürlich nur, wenn wir auch gute und saubere Unit Tests schreiben, die mit vernachlässigbarem Wartungsaufwand auskommen. Muss erstmal der Debugger im Unit Test Code angeworfen werden, geht die Sache aber nach hinten los: wir replizieren den Aufwand, den wir auch ohne Unit Tests schon hätten. Unit Tests sollen uns ja früh auf Fehler hinweisen, sodass wir erst gar nicht zum Debugger greifen müssen.

In den letzten Jahren ist mir eine Frage immer und immer wieder untergekommen, die sehr schnell eine Aussage über die Qualität eines Tests gibt:

Was testet dieser Test?

Haben Sie sich das schon einmal selbst gefragt? Ich bin immer wieder erstaunt, wie schwer es Entwicklern fallen kann, diese Frage zu beantworten.

Aussagen wie „Dieser Unit Test testet Methode X der Klasse Y“ sind hier nämlich nicht akzeptabel! Denken Sie am besten also erst gar nicht daran, sich von Ihrer IDE die Test-Stubs generieren zu lassen, denn dann erhalten Sie für jede (public) Methode Ihrer zu testenden Klasse genau einen Unit Test, der die Methode testen soll - in der Realität sehen Tests aber häufig anders aus, bzw. bedarf es pro Methode deutlich mehr als nur einen einzigen Test.

Eine gute Antwort auf die Frage, was dieser Test denn nun wirklich testet, muss mindestens die folgenden zwei Informationen enthalten:

  1. Was ist die Eingabe bzw. Ausgangssituation?
  2. Welches Ergebnis bzw. welche Ausgabe wird in diesem Fall erwartet?

Aufgepasst: Sobald in Ihrer Antwort ein Bindewort wie „und“ vorkommt, testet der Test zuviel! Man spricht hier auch von unfokussierten Tests, welche in zwei oder mehrere Tests aufgeteilt werden müssen, sodass für jeden der resultierenden Tests wiederum eine klare Antwort getroffen werden kann.

Ein konkretes Beispiel: Conway's Game of Life

Conway's Game of Life ist eine beliebte Programmieraufgabe, die u.a. jährlich auf dem Global Day of Code Retreat zur Übung von TDD verwendet wird.

Ausgehend von einer Klasse „GameOfLife“ mit vier öffentlichen Methoden, könnte ein Unit Test so aussehen:


public class GameOfLifeTest {

  @Test
  public void testNextGeneration() {
    // test code  
  }
  
  @Test
  public void testPopulateCellAt() {
    // test code
  }
  
  @Test
  public void testGetCells() {
    // test code
  }
  
  @Test
  public void testToString() {
    // test code
  }
}

Was testen nun die einzelnen Tests? Klar, sie testen die jeweilige Methode, aber diese Antwort reicht bei Weitem nicht aus. Auf den ersten Blick ist es auf jeden Fall schwierig, eine gute Antwort zu finden, ohne den Test Code näher betrachten zu müssen. Außerdem ist das „Test“-Suffix auf der Klasse nicht refactoring-safe, weshalb bei einer derartigen Benennung von Testklassen besondere Vorsicht geboten ist. Vermutlich wird es nicht lange dauern, bis ein Entwickler vergisst, dass er, nachdem er die Klasse umbenannt (refactored) hat, auch den Namen der Testklasse entsprechend anpassen muss.

Eine alternative Testklasse könnte beispielsweise so aussehen (Anm.: In diesem Fall gibt es mehrere Testklassen für das GameOfLife, diese Testklasse konzentriert darauf, was passiert, wenn das Spiel in die nächste Generation geht).


public class WhenTheGameProceedsToTheNextGeneration {

  @Test
  public void thenACellWithoutNeighborsDies() {
    // test code
  }
 
  @Test
  public void thenACellWithOneNeighborDies() {
    // test code 
  }
  
  @Test
  public void thenACellWithTwoNeighborsSurvives() 
  {
    // test code
  }

  @Test
  public void thenACellWithThreeNeighborsIsPopulated() {
    // test code
  }

  @Test
  public void thenACellWithMoreThanThreeNeigborsDies() {
    // test code
  }
}

Bei diesen Tests ist es deutlich einfacher, unsere Frage zu beantworten, da diese bereits in der Benennung beantwortet wird. Die Antwort auf den ersten Test lautet also: „Wenn das Spiel in die nächste Generation geht, stirbt eine Zelle ohne Nachbarn“ oder im dritten Test: „Wenn das Spiel in die nächste Generation geht, dann überlebt eine Zelle, die zwei Nachbarn hat.“

Fazit

Testnamen sollten nicht von der zu testenden Einheit abgeleitet sein, sondern klar ausdrücken, was ein Test testet. So werden Ihre Unit Tests zur lebenden Dokumentation und wenn etwas schiefgeht, weiß ein Entwickler sofort, welches Problem nun vorliegt.

Newsletter

Weitere interessante Artikel

Kontakt

Sie möchten sich unverbindlich über Ihr Softwareentwicklungs-Vorhaben austauschen? Erzählen Sie uns ein bisschen mehr!